[
  {
    "path": ".coveragerc",
    "content": "[run]\nbranch = True\n\nomit =\n    /tests/**\n    /docs/*\n    /*_tests/**\n\n[report]\n; Regexes for lines to exclude from consideration\nexclude_also =\n    ; Don't complain about missing debug-only code:\n    def __repr__\n    if self\\.debug\n\n    ; Don't complain if tests don't hit defensive assertion code:\n    raise AssertionError\n    raise NotImplementedError\n\n    ; Don't complain if non-runnable code isn't run:\n    if 0:\n    if __name__ == .__main__.:\n\n    ; Don't complain about abstract methods, they aren't run:\n    @(abc\\.)?abstractmethod\n\n    except adbutils.AdbError\n    @deprecated\n\nignore_errors = True\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE.md",
    "content": "**看完请删掉该内容**\n\n*提Bug需要注意的事项*\n\n请务必提供详细的信息，能够复现你的问题，否则很难帮你解决。没用的Issue将自动被机器人打上`Invalid`标签并且自动关闭！！。\n\n- 手机型号\n- uiautomator2的版本号(`pip show uiautomator2`)\n- 手机截图\n- 相关日志(Python控制台错误信息, adb logcat完整信息, atxagent.log日志)\n- 最好能附上可能复现问题的代码。\n"
  },
  {
    "path": ".github/copilot-instructions.md",
    "content": "# uiautomator2\n\nuiautomator2 is a Python library providing a simple, easy-to-use, and stable Android automation framework. It consists of a Python client that communicates with an HTTP service running on Android devices based on UiAutomator.\n\n**ALWAYS follow these instructions first and completely. Only fallback to additional search and context gathering if the information in these instructions is incomplete or found to be in error.**\n\nAlways reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here.\n\n## Working Effectively\n\n### Initial Setup\n- `pip install poetry` -- Install Poetry dependency manager\n- `poetry install` -- Install all dependencies in virtual environment. NEVER CANCEL: Takes 3-5 minutes. Set timeout to 8+ minutes.\n- Poetry will create a virtual environment in `.venv/` directory\n\n### Build and Test Process\n- `poetry run pytest tests/ -v` -- Run unit tests (25 tests). Takes ~3 seconds. All tests should pass.\n- `make cov` -- Run coverage tests. Takes ~3 seconds. Should show ~27% coverage.\n- `make format` -- Format code with isort. Takes ~1 second. ALWAYS run before committing.\n- `poetry build` -- Build distribution packages. Takes ~5 seconds. Creates wheel and sdist in `dist/`.\n- `poetry run uiautomator2 version` -- Check CLI functionality. Should output version number.\n\n### Asset Synchronization (Optional)\n- `make sync` -- Download required APK and JAR assets. FAILS due to network restrictions in sandboxed environment. This is EXPECTED and not required for development.\n- Asset sync downloads Android APK and u2.jar from external hosts which are blocked in this environment.\n\n### Commands That Will Fail (Expected)\n- `make test` -- Mobile tests require Android device via ADB. Will fail with \"Can't find any android device/emulator\" - this is EXPECTED.\n- `make build` -- Full build with poetry plugin. May fail due to system package conflicts. Use `poetry build` instead.\n- `make sync` -- Asset download fails due to network restrictions. Not required for core development.\n\n## Validation Scenarios\n\nAfter making changes, ALWAYS run this validation sequence:\n\n1. **Unit Tests**: `poetry run pytest tests/ -v` -- Must pass all 25 tests\n2. **Coverage**: `make cov` -- Should complete without errors\n3. **Formatting**: `make format` -- Always format before committing  \n4. **Build**: `poetry build` -- Must complete successfully\n5. **CLI Test**: `poetry run uiautomator2 --help` -- Should show help output\n\n### Manual Testing Scenarios\n- Test version command: `poetry run uiautomator2 version`\n- Test CLI help: `poetry run uiautomator2 --help`\n- Verify core imports: `poetry run python -c \"import uiautomator2; print('Import successful')\"`\n\n## Key Components and Structure\n\n### Core Modules (uiautomator2/)\n- `__init__.py` -- Main API and connection functions (426 lines, 27% coverage)\n- `xpath.py` -- XPath selector implementation (411 lines, 62% coverage)  \n- `_selector.py` -- UI element selectors (320 lines, 19% coverage)\n- `core.py` -- Core device interaction (214 lines, 21% coverage)\n- `watcher.py` -- Event watchers (212 lines, 20% coverage)\n\n### Test Directories\n- `tests/` -- Unit tests (25 tests, no device required)\n- `mobile_tests/` -- Integration tests (30 tests, require Android device)\n- `demo_tests/` -- Example/demo tests\n\n### Build Configuration\n- `pyproject.toml` -- Poetry configuration and dependencies\n- `Makefile` -- Build automation (format, test, build, sync commands)\n- `.coveragerc` -- Coverage configuration\n\n### Additional Components\n- `uibox/` -- Go component for Android binary tools (separate build system)\n\n### Documentation\n- `README.md` -- Main documentation with usage examples\n- `DEVELOP.md` -- Development setup instructions\n- `XPATH.md` -- XPath selector documentation\n- `CHANGELOG` -- Version history\n\n## Common Development Tasks\n\n### Adding New Features\n1. Run existing tests to ensure baseline: `poetry run pytest tests/ -v`\n2. Implement changes in appropriate module under `uiautomator2/`\n3. Add unit tests in `tests/` directory\n4. Run tests: `poetry run pytest tests/ -v`\n5. Format code: `make format`\n6. Check coverage: `make cov`\n7. Build to verify: `poetry build`\n\n### Debugging Issues\n- Enable debug logging: Use `-d` flag with CLI commands\n- Check import issues: `poetry run python -c \"import uiautomator2\"`\n- Device connection issues require actual Android device (expected to fail in this environment)\n\n### Code Style\n- Uses isort for import sorting with HANGING_INDENT mode and 120 character line length\n- Coverage requirement: Tests should maintain or improve the ~27% coverage baseline\n- All code must pass existing unit tests\n\n## Environment Limitations\n\n**CANNOT DO (Expected Failures):**\n- Mobile testing without Android device\n- Asset synchronization (network blocked)\n- Full make build (dependency conflicts)\n\n**CAN DO:**\n- Unit testing (tests/ directory)\n- Code formatting and linting\n- Building with `poetry build`\n- CLI testing and development\n- Core library development\n\n## Time Expectations\n\n- **NEVER CANCEL**: Poetry install takes 3-5 minutes. Set timeout to 8+ minutes.\n- Unit tests: ~2.5 seconds\n- Coverage tests: ~3.5 seconds  \n- Code formatting: ~0.5 seconds\n- Poetry build: ~3 seconds\n- Full validation sequence: ~12 seconds\n\n## Key Commands Reference\n\n```bash\n# Essential development workflow\npoetry install                    # Setup (3-5 min, NEVER CANCEL)\npoetry run pytest tests/ -v       # Unit tests (2.5s)\nmake format                       # Format (0.5s) \nmake cov                          # Coverage (3.5s)\npoetry build                      # Build (3s)\n\n# CLI testing\npoetry run uiautomator2 version   # Version check\npoetry run uiautomator2 --help    # Help system\n\n# Known failures (expected in sandboxed environment)\nmake test                         # Requires Android device\nmake sync                         # Network blocked\nmake build                        # Dependency conflicts\n```\n\nAlways validate changes with the full sequence: tests → format → coverage → build → CLI test.\n\n## Validation Guarantee\n\n**Every command in these instructions has been validated to work correctly.** If any command fails unexpectedly:\n\n1. First check that you're in the correct directory: `/path/to/uiautomator2`\n2. Ensure Poetry virtual environment is properly set up: `poetry install`\n3. Check for environment issues: `poetry run python -c \"import uiautomator2; print('OK')\"`\n4. If problems persist, the issue may be with your environment or changes you've made\n\nExpected validation results:\n- Unit tests: 25 tests should pass\n- Coverage: Should show ~27% total coverage  \n- Format: Should complete without errors (may show \"Skipped N files\")\n- Build: Should create `dist/` directory with wheel and sdist\n- CLI: Should display help text starting with \"usage: uiautomator2\""
  },
  {
    "path": ".github/workflows/main.yml",
    "content": "name: Python application\n\non:\n  push:\n    branches:\n      - master\n    tags-ignore:\n      - '*'\n  pull_request:\n    branches:\n      - master\n\njobs:\n  build-and-publish:\n    name: ${{ matrix.os }} / ${{ matrix.python-version }}\n    runs-on: ${{ matrix.image }}\n    strategy:\n      matrix:\n        os: [Ubuntu, Windows]\n        python-version: [\"3.8\", \"3.11\"]\n        include:\n          - os: Ubuntu\n            image: ubuntu-latest\n          - os: Windows\n            image: windows-2022\n          - os: macOS\n            image: macos-12\n      fail-fast: false\n    steps:\n    - uses: actions/checkout@v4\n\n    - name: Set up Python ${{ matrix.python-version }}\n      uses: actions/setup-python@v5\n      with:\n        python-version: ${{ matrix.python-version }}\n\n    - name: Get full Python version\n      id: full-python-version\n      run: echo version=$(python -c \"import sys; print('-'.join(str(v) for v in sys.version_info))\") >> $GITHUB_OUTPUT\n\n    - name: Update PATH\n      if: ${{ matrix.os != 'Windows' }}\n      run: echo \"$HOME/.local/bin\" >> $GITHUB_PATH\n\n    - name: Update Path for Windows\n      if: ${{ matrix.os == 'Windows' }}\n      run: echo \"$APPDATA\\Python\\Scripts\" >> $GITHUB_PATH\n\n    - name: Enable long paths for git on Windows\n      if: ${{ matrix.os == 'Windows' }}\n      # Enable handling long path names (+260 char) on the Windows platform\n      # https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file#maximum-path-length-limitation\n      run: git config --system core.longpaths true\n\n    - name: Install dependencies\n      run: |\n        python -m pip install --upgrade pip\n        pip install poetry\n        poetry install\n\n    - name: Run tests with coverage\n      run: |\n        make cov\n\n    - name: Upload test results to Codecov\n      if: ${{ !cancelled() }} # Run even if tests fail\n      uses: codecov/test-results-action@v1\n      with:\n        token: ${{ secrets.CODECOV_TOKEN }}\n\n    - name: Upload coverage reports to Codecov\n      uses: codecov/codecov-action@v4.0.1\n      with:\n        token: ${{ secrets.CODECOV_TOKEN }}\n        slug: openatx/uiautomator2\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release\n\non:\n  push:\n    tags:\n      - '*.*.*'\n\njobs:\n  release:\n    name: Release\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@v4\n\n    - name: Set up Python\n      uses: actions/setup-python@v4\n      with:\n        python-version: 3.8\n\n    - name: Install dependencies\n      run: |\n        python -m pip install --upgrade pip\n        pip install poetry\n\n    - name: Build\n      run: |\n        make build\n\n    - name: Publish distribution 📦 to PyPI\n      uses: pypa/gh-action-pypi-publish@release/v1\n      with:\n        skip-existing: true\n        password: ${{ secrets.PYPI_API_TOKEN }}"
  },
  {
    "path": ".gitignore",
    "content": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.idea/\n.Python\nenv/\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\n*.egg-info/\n.installed.cfg\n*.egg\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n.hypothesis/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# pyenv\n.python-version\n\n# celery beat schedule file\ncelerybeat-schedule\n\n# SageMath parsed files\n*.sage.py\n\n# dotenv\n.env\n\n# virtualenv\n.venv\nvenv/\nENV/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\nAUTHORS\nChangeLog\n\n.vscode/\nreport/\n*.apk\n*.exe\nnode_modules/\nvendor/\n\ndocs/*.rst\n\n.DS_Store\n*.lock\njunit.xml"
  },
  {
    "path": "CHANGELOG",
    "content": "CHANGES\n=======\n\n2.16.10\n-------\n\n* try not to reinstall apk when atx-agent is not installed\n\n2.16.9\n------\n\n* little fix for vivo and oppo, do not reinstall uiautomator apk\n\n2.16.8\n------\n\n* fix dump\\_hierarchy error when recovered in a minute\n* update logic for tmq\n* fixed: 增加app\\_install的超时时间 (#736)\n\n2.16.7\n------\n\n* use filelock to prevent multi process reset\\_uiautomator\n\n2.16.6\n------\n\n* remove process\\_safe\\_wrapper since not allow multi device operation\n\n2.16.5\n------\n\n* use filelock to make process call process safe\n\n2.16.4\n------\n\n* skip uninstall uiautomator apk for tmq platform\n* add link\n\n2.16.3\n------\n\n* use github actions to publish lib instead of trivis\n\n2.16.2\n------\n\n* fix tests\n* Update init.py (#618)\n\n2.16.1\n------\n\n* hotfix for multiprocess call reset\\_uiautomator\n* update ISSUE\\_TEMPLATE for REQUIRED logs\n* update doc\n\n2.16.0\n------\n\n* add cli:doctor\n* add doc\n\n2.15.2\n------\n\n* add support reconnect when device disconnect\n* update requirements\n* Update \\_\\_init\\_\\_.py (#679)\n\n2.15.1\n------\n\n* try to fix when wifi connect device still try to upgrade atx-agent bug\n* add multi thread example\n\n2.15.0\n------\n\n* add init --addr support\n* update func doc\n\n2.14.1\n------\n\n* fix init error\n\n2.14.0\n------\n\n* mark useless tests\n* add atx-agent version check when something when wrong\n* update apk and atx-agent version\n* skip flake8 check\n\n2.13.2\n------\n\n* update atx-agent to fix security error, ref openatx/atx-agent#82\n\n2.13.1\n------\n\n* update minicap download address to devicefarmer group, which support sdk:30\n\n2.13.0\n------\n\n* add d.xpath(..).child support\n\n2.12.3\n------\n\n* show float window in tmq platform\n\n2.12.2\n------\n\n* fix bug #650\n* add typing for image, commented findit\n\n2.12.1\n------\n\n* fix d.settings to self.settings\n* change localhost to 127.0.0.1\n\n2.12.0\n------\n\n* add open\\_url method\n\n2.11.5\n------\n\n* fix swipe set duration no effect, close #591\n\n2.11.4\n------\n\n* xpath: %xxx% support content-desc\n\n2.11.3\n------\n\n* add missing builtin arg\n* add builtin and autostart to watch\\_context\n* add hire doc\n\n2.11.2\n------\n\n* update requirements\n\n2.11.1\n------\n\n* fix settings props check\n\n2.11.0\n------\n\n* add watch\\_context which may replace watcher\n* fix reset-uiautomator on windows error\n\n2.10.2\n------\n\n* add retry for app\\_current, fix #572\n* update sponsor link\n\n2.10.1\n------\n\n* update tests, prevent atx-agent log too large\n\n2.10.0\n------\n\n* add more tests\n* add Direction, support scroll\\_to, update some doc\n* d.xpath add scroll support\n\n2.9.6\n-----\n\n* fix support for d(resourceId='android:id/text1')[-1].get\\_text()\n\n2.9.5\n-----\n\n* support change to production use os.environ['TMQ'] = true\n* raise EnvironmentError directly when connected with wifi, but atx-agent is down\n\n2.9.4\n-----\n\n* fix recover logic when atx-agent is not responsing\n\n2.9.3\n-----\n\n* enable screenrecord test\n* fix screenrecord\n\n2.9.2\n-----\n\n* fix wait\\_for\\_device not finished error\n\n2.9.1\n-----\n\n* fix selector long\\_click bug\n* update doc\n\n2.9.0\n-----\n\n* add operation\\_delay support\n\n2.8.6\n-----\n\n* add init into connect\\_usb for compability\n\n2.8.5\n-----\n\n* remove humanize\n* add support d(description=我的淘宝).screenshot()\n\n2.8.4\n-----\n\n* hotfix for set\\_new\\_command\\_timeout error\n\n2.8.3\n-----\n\n* hot fix for connect error when atx-agent not installed\n\n2.8.2\n-----\n\n* support fallback to WiFi when usb disconnected, add deprecated method :service\n\n2.8.1\n-----\n\n* fix app\\_start missing stop=True error\n* support push url\n\n2.8.0\n-----\n\n* change property serial back\n* add double\\_click, set click\\_pre and post delay to 0\n* fix bugs reported in qq\n* remove useless code\n* add missing swipe\\_ext and @address(teditor)\n* finally version\n* add missing toast\n* add more method\n* rewrite uiautomator2, too complex\n\n2.7.3\n-----\n\n* add timeout(60s) in init.py to prevent hang on apk install page\n\n2.7.2\n-----\n\n* update adbutils which buildin adb.exe for windows\n* rewrite part of init code\n\n2.7.1\n-----\n\n* upgrade adbutils: support download adb.exe when missing on windows\n\n2.7.0\n-----\n\n* add click\\_exists to xpath\n\n2.6.2\n-----\n\n* fix with reinstall apks when meet signature not matched error\n* add image.click doc and tests\n\n2.6.1\n-----\n\n* screenrecord support horizontal and vertical, support limit fps\n* add screenrecord usage\n\n2.6.0\n-----\n\n* add screenrecord code\n* add screenrecord sample\n\n2.5.9\n-----\n\n* upgrade atx-agent to 0.9.4 to fix go panic on go12\n\n2.5.8\n-----\n\n* update minicap sync method\n* update atx-agent version and apk version\n* call watcher when d.xpath calls\n* let d.touch.down support percent position, remove stop-app when reset-uiautomator\n* update doc\n* support Android Q minicap, show debug log when image search\n\n2.5.7\n-----\n\n* fix click on infinitly display not working bug\n* add recommended article\n* support generate all docs by sphinx\n* fix docs generate with sphinx, not very well\n* add missing file\n* fix retry when take screenshot, update readthedocs\n* add readthedocs for test\n\n2.5.6\n-----\n\n* add match and scroll\\_to to xpath object, update atx-agent version\n\n2.5.5\n-----\n\n* change connect\\_usb not start uiautomator automatically\n\n2.5.4\n-----\n\n* update atx-agent and apk version to use minitouchagent\n\n2.5.3\n-----\n\n\n2.5.2\n-----\n\n* fix pull error\n* add readTimeout handle\n\n2.5.1\n-----\n\n* fix \\_request func recursive error\n\n2.5.0\n-----\n\n* add d.alibaba support\n* update scale and wait-for-device timeout to 70s\n* fix when device replugin, d.shell fails\n\n2.4.6\n-----\n\n* fix wait am instrument too short, change timeout from 20 to 40\n* fix adbutils shell decode error\n* add retry in push\\_url\n\n2.4.5\n-----\n\n* fix usb cable replug raise ConnectionError bug\n\n2.4.4\n-----\n\n* update apk version, and atx-agent version\n* update atx-agent to 0.8.1, do lot of code format\n* fix Android Q screenshot error\n* fix init may raise FileNotFoundError bug\n* add uiautomator2 version in command line\n* add session test\n\n2.4.3\n-----\n\n* add fallback and session add some missing method\n* fix github workflow\n* fix flake8 warning\n* test github actions\n* change callback to fallback\n* add d.xpath(xxxxx).callback(click, px, py).click() support\n* add back token again\n* check if travis notification is working\n* add d.xpath.position方法\n\n2.4.2\n-----\n\n* change am instrument logic again\n* rewrite jsonrpc\\_retry\\_call logic\n* make recover uiautomator logic more simple\n\n2.4.1\n-----\n\n* add taobao plugin for internal network\n* add long\\_click to d.xpath\n\n2.4.0\n-----\n\n* change logic of start uiautomator, upgrade apk version\n* fix bug, reported by h.t\n* am start apk twice to make sure, uiautomator can be recovered\n\n2.3.4\n-----\n\n* show lib version when init for easily debug\n* support config service recover behavior\n\n2.3.3\n-----\n\n* fix d.serial return None bug, fix tests on large screen\n* update doc, add quick-reference.md\n* add quick ref guide\n\n2.3.2\n-----\n\n* fix init command not resolve signature mismatch bug, fix uninstall can not uninstall apk bug\n\n2.3.1\n-----\n\n* add xpath\\_debug to settings, fix xpath %xx and xx%\n* update watcher doc\n\n2.3.0\n-----\n\n* add d.watcher method to handle popups\n* add settings code\n* add basic settings.py\n* Update README.md\n* hotfix for windows\n* remove timeout for function: pull\n\n2.2.0\n-----\n\n* add cmd\\_purge, add set\\_new\\_command\\_timeout api\n\n2.1.0\n-----\n\n* add image.py, change uiautomator from v1 to v2\n* add uauto\n* typo (#476)\n* fix missing \\_parent error, close #477\n* hot fix for #475\n* fix spell error\n* fix logo not show error in readme\n* add hogwarts sponsor\n* add wait to image.py\n* fix xpath start-with and ends-with, add image click\n\n2.0.0\n-----\n\n* remove toast from readme\n* add app list api\n* support multi xpath(xx).xpath(xx), and add .info in xpath\n* add clipboard doc\n* change to uiautomator 1.0\n* Fixes #451\n* add clipboard support\n* Update README.md\n* fix d.xpath.when(..).when(..), thread-safe reset-uiautomator\n\n1.3.6\n-----\n\n* use monkey command to install apk on TMQ platform\n* fix d.xpath.watcher, fix d.shell can not handle & and ? bug\n\n1.3.5\n-----\n\n* add xpath.apply\\_watch\\_from\\_yaml, support xpath.when(1).when(2)\n* fix homepage link\n* fix atx-agent version compare check\n\n1.3.4\n-----\n\n* remove useless cli\n* use jsonrpc.dumpWindowsHierarchy instead of http GET /dump/hierarchy\n* assert file\\_size when cache\\_download\n\n1.3.3\n-----\n\n* fix uiautomator start error\n\n1.3.2\n-----\n\n* update atx-agent to fix UIAutomation not connected error\n* upgrade apk version\n* enhance reset\\_uiautomator()\n\n1.3.1\n-----\n\n* fix adbutils dep version\n\n1.3.0\n-----\n\n* fix check atx-agent\n* fix last commit\n* add function to check atx-agent version\n* update atx-agent version\n* update dingtalk webhook again\n* update dingtalk webhook\n\n1.2.6\n-----\n\n* fix when uiautomator not alive, func connect can not auto init error\n\n1.2.5\n-----\n\n* update dingtalk robot webhook url\n* set init as default, set default screenshot name when use cli:uiautomator2 screenshot\n* rename current\\_app to app\\_current\n* add webview for future develop\n\n1.2.4\n-----\n\n* fix app\\_start without activity not launch error\n* add adcd.py(abstract class about device) and implement pure adb to run test\n* implement pure adb to run test\n* use Baidu OCR to select element (#419)\n\n1.2.3\n-----\n\n* update androidbinary to fix momo can not start error #393\n* add support u2.connect\\_usb(serial, init=False)\n* change function behavior d.touch.up() to d.touch.up(x, y)\n\n1.2.2\n-----\n\n* fix app\\_list\\_running() only show 3rd party apps bug, add support to read from env-var ANDROID\\_SERIAL\n\n1.2.1\n-----\n\n* fix and add doc for app\\_start #425, add uiautomator check in dump\\_hierarchy\n* add thread lock in dump\\_hierarchy\n* fix session restart\n* Update README.md\n* add notification about dingtalk travis\n\n1.2.0\n-----\n\n* add wait gone\n* add strict argument to session()\n* rename UIAutomatorServer to Device, add session.restart() method\n* change http://tool.appetizer.io to https protocol\n* add swipe\\_ext('right', 0.9) method\n* add app\\_wait, app\\_list\\_running\n\n1.1.0\n-----\n\n* add swipe and screenshot to d.xpath element\n* fix  init with serial\n* update changelog, remove d.watchers.watched, use IPython.embed first in cmd:uiautomator2 console\n* add console in command line\n* fix shell(stream=True) timeout error, close #394\n\n1.0.3\n-----\n\n* fix android Q support again\n\n1.0.2\n-----\n\n* replace google-fire with argparse, add current, stop, start subcommand in command line\n* remove useless u2cli\n\n1.0.1\n-----\n\n* fix init unknown host service, close #373\n* add develop.md\n\n1.0.0\n-----\n\n* upgrade atx-agent version, and android-uiautomator-version, update doc\n* fix swipe\\_points usage in readme\n* init add mirror of appetizer\n* fix str decode error\n* fix debug mode decode error\n\n0.3.3\n-----\n\n* add watch\\_clear and address\n* add xpath.watch\\_stop()\n\n0.3.2\n-----\n\n* fix debug curl print\n* fix shell calls in connect\n\n0.3.1\n-----\n\n* fix #370\n* test with 3.5\n\n0.3.0\n-----\n\n* fix fix\n* fix travis again\n* fix travis\n* update readme\n* add missing dep:adbutils\n* update xpath doc, add set\\_text to xpath\n* remove uiautomator2/adbutils.py, use thirdparty adbutils\n* add quickstart, fix healthcheck for OnePlus\n* fix screenshot method\n* say goodbye to python2 and welcome python3\n* Update ISSUE\\_TEMPLATE.md\n* use /dump/hierarchy to instead of call:dumpHierarchy\n* update atx-agent version\n\n0.2.3\n-----\n\n* xpath element support click\n* add http\\_timeout for shell function, resolve #353\n* add xpath quicksheet\n* resolve #348\n* remove code which leads to minicap install error\n* add get method of xpath\n* add xpath::get\\_text(), close #337\n* add connect\\_adb\\_wifi function\n* add probot link\n* auto stale issue when tagged as invalid\n* serial support none\n* 修复多台设备时，list-forward失败 (#327)\n* \\`python -m uiautomator2 init\\`初始化403报错，增加header atx\\_agent\\_url中报错变量错误修复\n\n0.2.2\n-----\n\n* update atx-agent version\n* typo (#318)\n* fix connect\\_usb error\n\n0.2.1\n-----\n\n* fix #317, fix #316\n\n0.2.0\n-----\n\n* merge change\n* remove pure-python-adb dependency, use adbutils.py instead\n* format \\_\\_init\\_\\_.py, update adbutils with ADB Protocol\n* update changelog\n* part of job\n\n0.1.11\n------\n\n* limit pure-python-adb version, to fix from adb.client import error\n* support args\n\n0.1.10\n------\n\n* remove cmd:init from fire.Fire, fix forward error when muti device connect to one machine\n* upgrade atx-agent\n* ext\\_xpath support\n* remove 3.7\n* fix travis test again\n* fix travis\n* sort imports\n* split code to different files\n* Update README.md\n* Update README.md\n* remove debug with dict: which will lead misunderstanding\n* update atx-agent version\n* appveyor\n* exedir detection everywhere\n* fix\n* come at me\n* need android components nowadays\n* travis 2018 switches from android-21 to android-22\n* fix pip install requirements\n* fix travis lang\n* add emulator and tests to travis and update README\n* fix typo. (#278)\n\n0.1.9\n-----\n\n* fix connect\\_usb init error, close #276\n* fix typo\n* add set\\_fail\\_prompt function\n* add d.touch.(down|move|up) in readme\n* fix atxagent version code\n\n0.1.8\n-----\n\n* update atx-agent add api app\\_info, and app\\_icon\n* update atx-agent version to 0.5.1, fix session timeout error\n* update atx-agent version and netease music example\n* add wait\\_activity\n* raise IndexError when UiObject returned by child\\_by\\_xxx, close #261\n* fix xpath py2 py3 compatibale\n* fix xpath ext resource-id error\n* Update README.md (#260)\n* update weditor install method\n\n0.1.7\n-----\n\n* sem-ver:bugfix, fix init with PATH env error on windows\n* fix doc\n* update apk to 1.1.7 to fix dumpHierarchy, close #207\n\n0.1.6\n-----\n\n* use atx-agent server -stop before launch\n* force stop atx-agent when init\n* fix launch atx-agent with wrong PATH, which may cause /info get wrong info\n* fix test on android P emulator\n* 加入aricv图像识别插件 (#250)\n* update atx-agent version\n\n0.1.5\n-----\n\n* fix init, because of mirror down\n* fix xpath python2 support, perf create dir if not exists\n* fix little bug\n* update readme\n* first xpath plugin version\n* add more comment about xpath plugin\n* add xpath plugin\n\n0.1.4\n-----\n\n* update install method\n* update install part\n* add install test code\n* fix fps collect\n* update atx-agent version\n* fix if log bug in ext/info\n* 修改info插件调用模式 (#245)\n* add test info plugin (#240)\n* fix perf get data error (#239)\n* Update README.md\n* open python 3.7 support\n* 更改一处类型提示错误 (#229)\n* add beta method hooks\\_register\n* fix #206, init gives 'inf' as serial <class 'float'> (#216)\n* 修改init不成功的问题 (#221)\n* update to new atx-agent\n* fix current\\_app in sumsung, add tcp and udp in perf\n* add images\n* add fps\n* swipe duration default 0.1(old 0.5), add swipe ui\n* fix perf uiautomator in python2\n* update doc\n* fix perf d not exists bug\n* add traffic into perf plugin\n* update atx-agent version\n* catch AttributeError in UIAutomatorServer\n* add back implicitly wait\n* add perf doc\n* add perf plugin\n* runyaml fix\n* add plugin\\_register and ocr plugin\n* add plugin support\n* let shell return namedtuple, remove outdated docs\n* use q|query instead of xpath in steps\n* add send\\_action support\n* fix #200\n* add with into session, update oppo support\n* fix merge conflict\n* click add offset, support oppo install with browser\n* add oppo install method, not finished yet\n* fix str(err.data) encode error\n* Update \\_\\_init\\_\\_.py\n* add some comment\n* 1.修改截图定位线\n* raise error when error found in uiautomator2.cli install\n* catch NullPointerExceptionError on jsonrpc call\n* patch to catch UiAutomation not connect\n* use github-mirror for update-apk command\n* fix healthcheck\n* add unlock screen for healthcheck\n* add retry for objInfo\n* fix conflict\n* hot fix for update\\_instance\n* add implicit\\_wait function\n* remove pid file when stop atx-agent\n\n0.1.3\n-----\n\n* fix init twice error, update atx-agent t0 0.4.1\n* support vivo install\n* add cancel request support\n* fix python requires\n* update to new version\n* exclude py 3.7 version\n* make u2cli work\n* fix when no progress\n* update uiautomator2.cli install\n* show progress\n* add missing file\n* add u2cli entry\n* add qrcode of qq\n* add fail reason\n* todo: add push folder support\n* add --mirror document, ref #173\n* add retry for dump\\_hierarchy, because of UiDevice NullPointer Exception\n* support github-mirror to make download faster\n* chmod +x report bad mode on xiaomi HMNote3\n* Change method of detecting executable dir\n* merge openatx\n* fix push to /data/local/tmp/mini... instead of /data/local/tmp\n* fix requests RemoteDisconnected error\n* Use pure-python-adb to get serials of all android devices when initializing\n* If adb client can't connect to the adb server, try to use adb cli to start adb server\n* Use pure-python-adb package to replace adb wrapper\n* support --mirror\n* fix get toast error\n* hot fix for executable dir\n* replace $ into -, fix #152\n* update document\n* use /data/local/tmp as default exec dir\n* forgot to update apk version\n* manually merge pr 46\n* parens are necessary to catch multi exception in python3\n* add screenshot(format=raw), fix init timeoutError, close #114\n* Replace os.path.join with string format, so can run as normal on windows\n* Revert changes to install\\_atx\\_agent\n* Provide alternative execute directory to /data/local/tmp, so can install to devices like 'ZUK's Z2\n* Solve ZUK's no permission to /data/local/tmp problem\n* fix xpath wait, fix connect simulator bug, update apk, to make watchers faster\n* Replace os.path.join with string format, so can run as normal on windows\n* Revert changes to install\\_atx\\_agent\n* Provide alternative execute directory to /data/local/tmp, so can install to devices like 'ZUK's Z2\n* hot fix for session launch\n* fix fix\n* update apk version to fix #138 #137\n* update view\n* add xpath support\n* fix session can not start app error\n* start atx-agent if atx-agent dead when connect\\_usb\n* fix ext/htmlreport unpatch\n* exists return class, fix watchers.watched not working bug\n* add toast capture support\n* add d.watchers.watched = True support\n\n0.1.2\n-----\n\n* Import update on uiautomator-server, fix current app function fix #41\n* \\_wait\\_install\\_finished 增加 hasattr(sys.stdout, 'isatty')判断\n* fix current\\_ime() failed\n* Solve ZUK's no permission to /data/local/tmp problem\n* add shell function in order to replace adb\\_shell one day\n* support long running command\n* package info should return None\n* comment useless code\n* update apk version, try to catch NullException\n* run code again for NullObjectException and StaleObjectException\n* fix install -g error\n* handle StaleObjectException\n* fix dns when network change\n* only build in python 2.7\n* add healthcheck in command line\n* update travis\n* format code, add click\\_gone function\n* change prompt\n* add double click support\n* add proxyhttp.go not finished yet\n* stash code\n* add support to patch long\\_click\n* add fancybox into htmlreport\n* add qqicon\n\n0.1.1\n-----\n\n* fix message in None error\n* try to fix #73\n* update atx-agent version\n* add screenshot into cli\n* fix for failed to init\n* modified for android simulator\n* add docstring for swipe\\_points\n* add swipe points description\n* add --ignore-apk-check option\n* add issue template\n* little fix\n* wait disable\\_popups for fix\n* UiObject support long\\_click with duration\n* add issue robot\n* support back to init multi devices\n* if adb without -g, remove -g and try again\n* add DeleteImmediatelly in disable\\_popups\n* update apk version to support toast\n* add support to show toast\n* add how to do with popups\n* update version\n* add disable\\_popups support\n* update atx agent\n* change TMPDIR to support upload large file\n* fix UINotFoundEncoding error\n* check if apk installed after init\n* open u2 github URL after success init\n* add adbkit-init\n* fix raise exception unicode code encode error\n* fix click\\_nowait missing error\n* support stop uiautomator keeper\n* fix htmlreport\n* add some useful link\n* add htmlreport support, remove click\\_nowait and tap\n\n0.1.0\n-----\n\n* add session support\n* add syntax error retry on screenshot error\n* hot fix to fix atx-agent screenshot bug\n* 修改import错误 ：ImportError: cannot import name popup\n* update atx-agent version\n* send\\_keys use adb shell input text when set\\_fast\\_ime failed.  upgrade pos\\_rel2abs  function\n* add tkgui for experiment\n* show better app\\_install progress on noatty, make healthcheck better\n* update TOC\n* sync to atx-agent new download logic\n* travis fight\n* no android for now\n* boring travis non-python pip problem\n* fix travis build\n* add Android emulator to travis and deploy only once on py2.7\n* clarify adb\\_shell; fix typos\n* Update README.md\n* fix healthcheck on xiaomi device\n\n0.0.3\n-----\n\n* fix apk version name\n* hot fix\n* not raise RuntimeError in current\\_app()\n* add window\\_size api\n* remove ReadTimeout from jsonrpc\\_retry\\_call\n* update logic, when uiautomator2 is down, restart apk\n* fix input method\n* add timeout in screenshot and restart uiautomator.apk shen connect 502\n* hot fix for weditor\n* stop uiautomator before start when do healthcheck()\n* open identify activity with am start -n\n* fix deprecated warn error\n* deprecated set\\_click\\_post\\_delay\n* add deault wait\\_timeout  set support\n* add retry to prevent screenshot error on some special conditions\n* update screenshot to support opencv\n* update atx agent version\n* update the connect method\n* update atx-agent version\n* add push\\_url api\n* 增加init时对代理的支持\n* support install on emulator\n* suppress warning when uninstall error\n* rename examples/powerweb to webbattery\n* add webpower ^\\_^\n* fix displayHeight error on Huawei\n* update atx\\_agent version to 0.1.1\n* make pos\\_rel2abs a little faster\n* modify http\\_timeout according to wait(timeout..)\n\n0.0.2\n-----\n\n* update doc\n* update doc\n* support oppo auto install\n* add app\\_install\\_local, handle serial contains &\n* swipe\\_points support percent points\n* long click support seconds\n* add minitouch install support\n* add minitouch but not tested\n* add FastInputIME\n* add send\\_keys method\n* guesture relative pos to real, close #12\n* fix click\\_exists\n* add gesture and pinch\n* add select count and fling, scroll\n* update ABOUT.rst addr\n\n0.0.1\n-----\n\n* setup travis build on all\\_branches\n* add skip cleanup\n* update doc again\n* check com.github.uiautotor.test when init\n* update badge link\n* fix datetime error\n* add debug\n* add identify method\n* add default timeout to requests\n* update to new version\n* change healthcheck logic, launch com.github.uiautomator and then HOME\n* update atx-agent version to 0.0.9\n* sync with atx-agent code\n* when device ip is empty, connect\\_usb will be called\n* add pull support\n* support stop in app\\_start\n* add app-stop-all method\n* add unlock cli\n* add watcher support\n* update install guide\n* add pypi version badge\n* add readme\n* am\\_start add stop param\n* click when exists\n* add healthcheck and connect\\_usb, close #3\n* add unlock method\n* add delay after click\n* fix abilist is empty error\n* add session check(check if app is alive when test is running\n* fix atx-agent install error\n* add clear cache support\n* add pushfile support\n* support kill all apps\n* support percent positions\n* fix detect device from adb devices -l error\n* remove useless print\n* support init multi devices\n* support percent tap, recode init logic\n* fix raise UiObjectNotFoundError error\n* fix incompatible in py3\n* tired, want to sleep\n* add output\n* fix auto install method\n* add auto install requirements scripts\n* update document\n* screenshot return PIL.Image\n* ref |> update function app\\_start(..) can input packagename and activity to start app\n* update doc to lastest\n* add selector long\\_click, update some doc\n* add example test\n* set default port to 7912\n* update readme\n* add connect(..) and add some doc\n* fix some error\n* initial project\n* Initial commit\n"
  },
  {
    "path": "DEVELOP.md",
    "content": "## Local development\n\n```\ngit clone https://github.com/openatx/uiautomator2\ncd uiautomator2\n\npip install poetry\npoetry install\n\n# download apk to assets/\nmake sync\n\n# run python shell after device or emulator connected\npoetry run uiautomator2 console\n```\n\n\n## ViewConfiguration\nDefault configuration can retrived from [/android/view/ViewConfiguration.java](https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/view/ViewConfiguration.java)\n\n> Unit: ms\n\n- TAP_TIMEOUT: 100\n- LONG_PRESS_TIMEOUT: 500\n- DOUBLE_TAP_TIMEOUT: 300\n"
  },
  {
    "path": "HISTORY.md",
    "content": "## 项目背景\n\n大约在2017年的时候，我在做Android自动化相关的工作，当时的脚本是用的Python写的，所以去网上找了下相关的开源项目。\n\n刚好找到了 https://github.com/xiaocong/uiautomator\n原理是在手机上运行了一个http rpc服务，将uiautomator中的功能开放出来，然后再将这些http接口封装成Python库。这个库写的实在是太好了，爱不释手。\n但是这个项目很久也没更新了，也联系不上作者，于是我就fork了一个版本\n为了方便做区分我们就在后面加了个2，从uiautomator变成了uiautomator2\n\n- [openatx/uiautomator2](https://github.com/openatx/uiautomator2)\n- [openatx/android-uiautomator-server](https://github.com/openatx/android-uiautomator-server)\n\n增加了各种各样的代码，对其中的bug做了修复。\n\n期间也衍生出来的很多其他项目\n\n- 自动化工具 https://github.com/NeteaseGame/ATX 废弃\n- 设备管理平台(也支持iOS) [atxserver2](https://github.com/openatx/atxserver2) 废弃\n- 纯Python的ADB客户端 https://github.com/openatx/adbutils 这个还健康的存活着\n- https://github.com/openatx/weditor 不维护了，不过有开发了一个新的。 https://uiauto.dev\n- [uiauto.dev](https://uiauto.dev) 用于查看UI层级结构，类似于uiautomatorviewer(用于替代之前写的weditor），用于查看UI层级结构 \n\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2017 openatx\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "Makefile",
    "content": ".PHONY: build\n\nformat:\n\tpoetry run isort . -m HANGING_INDENT -l 120\n\ntest:\n\tpoetry run pytest -v mobile_tests/\n\ncovtest:\n\tpoetry run coverage run -m pytest -v demo_tests tests\n\tpoetry run coverage html --include 'uiautomator2/**'\n\n\ncov:\n\tpoetry run pytest -v tests/ \\\n\t\t\t--cov-config=.coveragerc \\\n\t\t\t--cov uiautomator2 \\\n\t\t\t--cov-report xml \\\n\t\t\t--cov-report term \\\n\t\t\t--junitxml=junit.xml -o junit_family=legacy\n\n\nsync:\n\tcd uiautomator2/assets; ./sync.sh; cd -\n\nbuild:\n\tpoetry self add \"poetry-dynamic-versioning[plugin]\"\n\tcd uiautomator2/assets; ./sync.sh; cd -\n\trm -fr dist\n\tpoetry build -vvv\n\ninit:\n\tif [ ! -f \"ApiDemos-debug.apk\" ]; then \\\n\t\twget https://github.com/appium/appium/raw/master/packages/appium/sample-code/apps/ApiDemos-debug.apk; \\\n\tfi\n\tpoetry run python -m adbutils -i ./ApiDemos-debug.apk\n\n"
  },
  {
    "path": "QUICK_REFERENCE.md",
    "content": "# QUICK REFENRECE GUIDE\n\n```python\nimport uiautomator2 as u2\n\nd = u2.connect(\"--serial-here--\") # 只有一个设备也可以省略参数\nd = u2.connect() # 一个设备时, read env-var ANDROID_SERIAL\n\n# 信息获取\nprint(d.info)\nprint(d.device_info)\nwidth, height = d.window_size()\nprint(d.wlan_ip)\nprint(d.serial)\n\n## 截图\nd.screenshot() # Pillow.Image.Image格式\nd.screenshot().save(\"current_screen.jpg\")\n\n# 获取hierarchy\nd.dump_hierarchy() # str\n\n# 设置查找元素等待时间，单位秒\nd.implicitly_wait(10)\n\nd.app_current() # 获取前台应用 packageName, activity\nd.app_start(\"io.appium.android.apis\") # 启动应用\nd.app_start(\"io.appium.android.apis\", stop=True) # 启动应用前停止应用\nd.app_stop(\"io.appium.android.apis\") # 停止应用\n\napp = d.session(\"io.appium.android.apis\") # 启动应用并获取session\n\n# session的用途是操作的同时监控应用是否闪退，当闪退时操作，会抛出SessionBrokenError\napp.click(10, 20) # 坐标点击\n\n# 无session状态下操作\nd.click(10, 20) # 坐标点击\nd.long_click(10, 10)\nd.double_click(10, 20)\n\nd.swipe(10, 20, 80, 90) # 从(10, 20)滑动到(80, 90)\nd.swipe_ext(\"right\") # 整个屏幕右滑动\nd.swipe_ext(\"right\", scale=0.9) # 屏幕右滑，滑动距离为屏幕宽度的90%\nd.drag(10, 10, 80, 80)\n\nd.press(\"back\") # 模拟点击返回键\nd.press(\"home\") # 模拟Home键\nd.long_press(\"volume_up\")\n\nd.send_keys(\"hello world\") # 模拟输入，需要光标已经在输入框中才可以\nd.clear_text() # 清空输入框\n\nd.screen_on() # wakeUp\nd.screen_off() # sleep screen\n\nprint(d.orientation) # left|right|natural|upsidedown\nd.orientation = 'natural'\nd.freeze_rotation(True)\n\nprint(d.last_toast) # 获取显示的toast文本\nd.clear_toast() # 重置一下\n\nd.open_notification()\nd.open_quick_settings()\n\nd.open_url(\"https://www.baidu.com\")\nd.keyevent(\"HOME\") # same as: input keyevent HOME\n\n# 执行shell命令\noutput, exit_code = d.shell(\"ps -A\", timeout=60) # 执行shell命令，获取输出和exitCode\noutput = d.shell(\"pwd\").output # 这样也可以\nexit_code = d.shell(\"pwd\").exit_code # 这样也可以\n\n# Selector操作\nsel = d(text=\"Gmail\")\nsel.wait()\nsel.click()\n\n```\n\n```python\n# XPath操作\n# 元素操作\nd.xpath(\"立即开户\").wait() # 等待元素，最长等10s（默认）\nd.xpath(\"立即开户\").wait(timeout=10) # 修改默认等待时间\n\n# 常用配置\nd.settings['wait_timeout'] = 20 # 控件查找默认等待时间(默认20s)\n\nd.xpath(\"立即开户\").click() # 包含查找等待+点击操作，匹配text或者description等于立即开户的按钮\nd.xpath(\"//*[@text='私人FM']/../android.widget.ImageView\").click()\n\nd.xpath('//*[@text=\"私人FM\"]').get().info # 获取控件信息\n\nfor el in d.xpath('//android.widget.EditText').all():\n    print(\"rect:\", el.rect) # output tuple: (left_x, top_y, width, height)\n    print(\"bounds:\", el.bounds) # output tuple: （left, top, right, bottom)\n    print(\"center:\", el.center())\n    el.click() # click operation\n    print(el.elem) # 输出lxml解析出来的Node\n\n# 监控弹窗(在线程中监控)\nd.watcher.when(\"跳过\").click()\nd.watcher.start()\n```\n\n**欢迎多提意见。更欢迎Pull Request**"
  },
  {
    "path": "README.md",
    "content": "<!-- filepath: /Users/codeskyblue/Codes/uiautomator2/README.md -->\n# uiautomator2\n\n[![PyPI](https://img.shields.io/pypi/v/uiautomator2.svg)](https://pypi.python.org/pypi/uiautomator2)\n![PyPI](https://img.shields.io/pypi/pyversions/uiautomator2.svg)\n[![codecov](https://codecov.io/gh/openatx/uiautomator2/graph/badge.svg?token=d0ZLkqorBu)](https://codecov.io/gh/openatx/uiautomator2)\n\n[📖 Read the Chinese version](README_CN.md)\n\nA simple, easy-to-use, and stable Android automation library.\n\n- QQ Group: 815453846\n- Discord: <https://discord.gg/PbJhnZJKDd>\n\n> Users still on version 2.x.x, please check [2to3](docs/2to3.md) before deciding to upgrade to 3.x.x (Upgrade is highly recommended).\n\n## How it Works\nThis framework mainly consists of two parts:\n\n1.  **Device Side**: Runs an HTTP service based on UiAutomator, providing various interfaces for Android automation.\n2.  **Python Client**: Communicates with the device side via HTTP protocol, invoking UiAutomator's various functions.\n\nSimply put, it exposes Android automation capabilities to Python through HTTP interfaces. This design makes Python-side code writing simpler and more intuitive.\n\n# Dependencies\n- Android version 4.4+\n- Python 3.8+\n\n# Installation\n\n```sh\npip install uiautomator2\n\n# Check if installation was successful, normally it will output the library version\nuiautomator2 version\n# or: python -m uiautomator2 version\n```\n\nInstall element inspection tool (optional, but highly recommended):\n\n> For more detailed usage instructions, refer to: https://github.com/codeskyblue/uiautodev QQ:536481989\n\n```sh\npip install uiautodev\n\n# After starting from the command line, it will automatically open the browser\nuiautodev\n# or: python -m uiautodev\n```\n\nAlternatives: uiautomatorviewer, Appium Inspector\n\n# Quick Start\n\nPrepare an Android phone with `Developer options` enabled, connect it to the computer, and ensure that `adb devices` shows the connected device.\n\nOpen a Python interactive window. Then, input the following commands into the window.\n\n```python\nimport uiautomator2 as u2\n\nd = u2.connect() # Specify device serial number if multiple devices are connected\nprint(d.info)\n# Expected output\n# {'currentPackageName': 'net.oneplus.launcher', 'displayHeight': 1920, 'displayRotation': 0, 'displaySizeDpX': 411, 'displaySizeDpY': 731, 'displayWidth': 1080, 'productName': 'OnePlus5', 'screenOn': True, 'sdkInt': 27, 'naturalOrientation': True}\n```\n\nExample script:\n\n```python\nimport uiautomator2 as u2\n\nd = u2.connect('Q5S5T19611004599')\nd.app_start('tv.danmaku.bili', stop=True) # Start Bilibili\nd.wait_activity('.MainActivityV2')\nd.sleep(5) # Wait for splash screen ad to disappear\nd.xpath('//*[@text=\"我的\"]').click() # Click \"My\"\n# Get fan count\nfans_count = d.xpath('//*[@resource-id=\"tv.danmaku.bili:id/fans_count\"]').text\nprint(f\"Fan count: {fans_count}\")\n```\n\n# Documentation\n\n## Connecting to Device\n\nMethod 1: Connect using device serial number, e.g., `Q5S5T19611004599` (seen from `adb devices`)\n\n```python\nimport uiautomator2 as u2\n\nd = u2.connect('Q5S5T19611004599') # alias for u2.connect_usb('123456f')\nprint(d.info)\n```\n\nMethod 2: Serial number can be passed via environment variable `ANDROID_SERIAL`\n\n```python\n# export ANDROID_SERIAL=Q5S5T19611004599\nd = u2.connect()\n```\n\nMethod 3: Specify device via transport_id\n\n```sh\n$ adb devices -l\nQ5S5T19611004599       device 0-1.2.2 product:ELE-AL00 model:ELE_AL00 device:HWELE transport_id:6\n```\n\nHere you can see `transport_id:6`.\n\n> You can also get all connected transport_ids via `adbutils.adb.list(extended=True)`\n> Refer to https://github.com/openatx/adbutils\n\n```python\nimport adbutils # Requires version >=2.9.1\nimport uiautomator2 as u2\ndev = adbutils.device(transport_id=6)\nd = u2.connect(dev)\n```\n\n## Operating Elements with XPath\n\nWhat is XPath:\n\nXPath is a query language for locating content in XML or HTML documents. It uses simple syntax rules to establish a path from the root node to the desired element.\n\nBasic Syntax:\n- `/` - Select from the root node\n- `//` - Select from any position starting from the current node\n- `.` - Select the current node\n- `..` - Select the parent of the current node\n- `@` - Select attributes\n- `[]` - Predicate expression, used for filtering conditions\n\nYou can quickly generate XPath using [UIAutoDev](https://uiauto.dev).\n\nCommon Usage:\n\n```python\nd.xpath('//*[@text=\"私人FM\"]').click() # Click element with text \"私人FM\"\n\n# Syntactic sugar\nd.xpath('@personal-fm') # Equivalent to d.xpath('//*[@resource-id=\"personal-fm\"]')\n\nsl = d.xpath(\"@com.example:id/home_searchedit\") # sl is an XPathSelector object\nsl.click()\nsl.click(timeout=10) # Specify timeout, throws XPathElementNotFoundError if not found\nsl.click_exists() # Click if exists, returns whether click was successful\nsl.click_exists(timeout=10) # Wait up to 10s\n\n# Wait for the corresponding element to appear, returns XMLElement\n# Default wait time is 10s\nel = sl.wait()\nel = sl.wait(timeout=15) # Wait 15s, returns None if not found\n\n# Wait for element to disappear\nsl.wait_gone()\nsl.wait_gone(timeout=15)\n\n# Similar to wait, but throws XPathElementNotFoundError if not found\nel = sl.get()\nel = sl.get(timeout=15)\n\nsl.get_text() # Get component text\nsl.set_text(\"\") # Clear input field\nsl.set_text(\"hello world\") # Input \"hello world\" into input field\n```\n\nFor more usage, refer to [XPath Interface Document](XPATH.md)\n\n## Plugins\n\n- webview: https://github.com/YuYoungG/uiautomator2-webview\n\nTo maintain the project's simplicity and extensibility, future plugins will be integrated as third-party libraries.\n\n## Operating Elements with UiAutomator API\n\n### Element Wait Timeout\nSet element search wait time (default 20s)\n\n```python\nd.implicitly_wait(10.0) # Can also be modified via d.settings['wait_timeout'] = 10.0\nprint(\"wait timeout\", d.implicitly_wait()) # get default implicit wait\n\n# Throws UiObjectNotFoundError if \"Settings\" does not appear in 10s\nd(text=\"Settings\").click()\n```\n\nWait timeout affects the following functions: `click`, `long_click`, `drag_to`, `get_text`, `set_text`, `clear_text`.\n\n### Get Device Information\n\nInformation obtained via UiAutomator:\n\n```python\nd.info\n# Output\n{'currentPackageName': 'com.android.systemui',\n 'displayHeight': 1560,\n 'displayRotation': 0,\n 'displaySizeDpX': 360,\n 'displaySizeDpY': 780,\n 'displayWidth': 720,\n 'naturalOrientation': True,\n 'productName': 'ELE-AL00',\n 'screenOn': True,\n 'sdkInt': 29}\n```\n\nGet device information (based on `adb shell getprop` command):\n\n```python\nprint(d.device_info)\n# output\n{'arch': 'arm64-v8a',\n 'brand': 'google',\n 'model': 'sdk_gphone64_arm64',\n 'sdk': 34,\n 'serial': 'EMULATOR34X1X19X0',\n 'version': 14}\n```\n\nGet screen physical size (depends on `adb shell wm size`):\n\n```python\nprint(d.window_size())\n# device upright output example: (1080, 1920)\n# device horizontal output example: (1920, 1080)\n```\n\nGet current App (depends on `adb shell`):\n\n```python\nprint(d.app_current())\n# Output example 1: {'activity': '.Client', 'package': 'com.netease.example', 'pid': 23710}\n# Output example 2: {'activity': '.Client', 'package': 'com.netease.example'}\n# Output example 3: {'activity': None, 'package': None}\n```\n\nWait for Activity (depends on `adb shell`):\n\n```python\nd.wait_activity(\".ApiDemos\", timeout=10) # default timeout 10.0 seconds\n# Output: true or false\n```\n\nGet device serial number:\n\n```python\nprint(d.serial)\n# output example: 74aAEDR428Z9\n```\n\nGet device WLAN IP (depends on `adb shell`):\n\n```python\nprint(d.wlan_ip)\n# output example: 10.0.0.1 or None\n```\n\n### Clipboard\nSet or get clipboard content.\n\n* clipboard/set_clipboard\n\n    ```python\n    # Set clipboard\n    d.clipboard = 'hello-world'\n    # or\n    d.set_clipboard('hello-world', 'label')\n\n    # Get clipboard\n    # Depends on input method (com.github.uiautomator/.AdbKeyboard)\n    d.set_input_ime()\n    print(d.clipboard)\n    ```\n\n### Key Events\n\n* Turn on/off screen\n\n    ```python\n    d.screen_on() # turn on the screen\n    d.screen_off() # turn off the screen\n    ```\n\n* Get current screen status\n\n    ```python\n    d.info.get('screenOn')\n    ```\n\n* Press hard/soft key\n\n    ```python\n    d.press(\"home\") # press the home key, with key name\n    d.press(\"back\") # press the back key, with key name\n    d.press(0x07, 0x02) # press keycode 0x07('0') with META ALT(0x02)\n    ```\n\n* These key names are currently supported:\n\n    - home\n    - back\n    - left\n    - right\n    - up\n    - down\n    - center\n    - menu\n    - search\n    - enter\n    - delete ( or del)\n    - recent (recent apps)\n    - volume_up\n    - volume_down\n    - volume_mute\n    - camera\n    - power\n\nYou can find all key code definitions at [Android KeyEvent](https://developer.android.com/reference/android/view/KeyEvent.html)\n\n* Unlock screen\n\n    ```python\n    d.unlock()\n    # This is equivalent to\n    # 1. press(\"power\")\n    # 2. swipe from left-bottom to right-top\n    ```\n\n### Gesture interaction with the device\n* Click on the screen\n\n    ```python\n    d.click(x, y)\n    ```\n\n* Double click\n\n    ```python\n    d.double_click(x, y)\n    d.double_click(x, y, 0.1) # default duration between two clicks is 0.1s\n    ```\n\n* Long click on the screen\n\n    ```python\n    d.long_click(x, y)\n    d.long_click(x, y, 0.5) # long click 0.5s (default)\n    ```\n\n* Swipe\n\n    ```python\n    d.swipe(sx, sy, ex, ey)\n    d.swipe(sx, sy, ex, ey, 0.5) # swipe for 0.5s (default)\n    ```\n\n* SwipeExt (Extended functionality)\n\n    ```python\n    d.swipe_ext(\"right\") # Swipe right, 4 options: \"left\", \"right\", \"up\", \"down\"\n    d.swipe_ext(\"right\", scale=0.9) # Default 0.9, swipe distance is 90% of screen width\n    d.swipe_ext(\"right\", box=(0, 0, 100, 100)) # Swipe within the area (0,0) -> (100, 100)\n\n    # In practice, starting swipe from the midpoint for up/down swipes has a higher success rate\n    d.swipe_ext(\"up\", scale=0.8)\n\n    # Can also use Direction as a parameter\n    from uiautomator2 import Direction\n    \n    d.swipe_ext(Direction.FORWARD) # Scroll down page, equivalent to d.swipe_ext(\"up\"), but easier to understand\n    d.swipe_ext(Direction.BACKWARD) # Scroll up page\n    d.swipe_ext(Direction.HORIZ_FORWARD) # Scroll page horizontally right\n    d.swipe_ext(Direction.HORIZ_BACKWARD) # Scroll page horizontally left\n    ```\n\n* Drag\n\n    ```python\n    d.drag(sx, sy, ex, ey)\n    d.drag(sx, sy, ex, ey, 0.5) # drag for 0.5s (default)\n    ```\n\n* Swipe points\n\n    ```python\n    # swipe from point(x0, y0) to point(x1, y1) then to point(x2, y2)\n    # time will be 0.2s between two points\n    d.swipe_points([(x0, y0), (x1, y1), (x2, y2)], 0.2)\n    ```\n\n    Often used for pattern unlock, get relative coordinates of each point beforehand (supports percentages).\n    For more detailed usage, refer to this post [Using u2 to implement pattern unlock](https://testerhome.com/topics/11034)\n\n* Touch and drag (Beta)\n\n    This is a lower-level raw interface, feels incomplete but usable. Note: percentages are not supported here.\n\n    ```python\n    d.touch.down(10, 10) # Simulate press down\n    time.sleep(.01) # Delay between down and move, control it yourself\n    d.touch.move(15, 15) # Simulate move\n    d.touch.up(10, 10) # Simulate release\n    ```\n\nNote: click, swipe, drag operations support percentage position values. Example:\n\n`d.long_click(0.5, 0.5)` means long click center of screen.\n\n### Screen Related APIs\n* Retrieve/Set device orientation\n\n    The possible orientations:\n\n    -   `natural` or `n`\n    -   `left` or `l`\n    -   `right` or `r`\n    -   `upsidedown` or `u` (cannot be set)\n\n    ```python\n    # retrieve orientation. the output could be \"natural\" or \"left\" or \"right\" or \"upsidedown\"\n    orientation = d.orientation\n\n    # WARNING: did not pass testing on my TT-M1\n    # set orientation and freeze rotation.\n    # notes: setting \"upsidedown\" requires Android>=4.3.\n    d.set_orientation('l') # or \"left\"\n    d.set_orientation(\"r\") # or \"right\"\n    d.set_orientation(\"n\") # or \"natural\"\n    ```\n\n* Freeze/Un-freeze rotation\n\n    ```python\n    # freeze rotation\n    d.freeze_rotation()\n    # un-freeze rotation\n    d.freeze_rotation(False)\n    ```\n\n* Take screenshot\n\n    ```python\n    # take screenshot and save to a file on the computer, requires Android>=4.2.\n    d.screenshot(\"home.jpg\")\n    \n    # get PIL.Image formatted images. Naturally, you need Pillow installed first\n    image = d.screenshot() # default format=\"pillow\"\n    image.save(\"home.jpg\") # or home.png. Currently, only png and jpg are supported\n\n    # get OpenCV formatted images. Naturally, you need numpy and cv2 installed first\n    import cv2\n    image = d.screenshot(format='opencv')\n    cv2.imwrite('home.jpg', image)\n\n    # get raw jpeg data\n    imagebin = d.screenshot(format='raw')\n    with open(\"some.jpg\", \"wb\") as f:\n        f.write(imagebin)\n    ```\n\n* Dump UI hierarchy\n\n    ```python\n    # get the UI hierarchy dump content\n    xml = d.dump_hierarchy()\n\n    # compressed=True: include non-important nodes (default False)\n    # pretty: format xml (default False)\n    # max_depth: limit xml depth, default 50\n    xml = d.dump_hierarchy(compressed=False, pretty=True, max_depth=30)\n    ```\n\n* Open notification or quick settings\n\n    ```python\n    d.open_notification()\n    d.open_quick_settings()\n    ```\n\n* Show touch trace on device screen\n\n    ```python\n    # show touch trace on device screen\n    d.show_touch_trace()\n    # hide touch trace\n    d.show_touch_trace(pointer_location=False, show_touches=False)\n    ```\n\n### Selector\n\nSelector is a handy mechanism to identify a specific UI object in the current window.\n\n```python\n# Select the object with text 'Clock' and its className is 'android.widget.TextView'\nd(text='Clock', className='android.widget.TextView')\n```\n\nSelector supports the below parameters. Refer to [UiSelector Java doc](http://developer.android.com/tools/help/uiautomator/UiSelector.html) for detailed information.\n\n*  `text`, `textContains`, `textMatches`, `textStartsWith`\n*  `className`, `classNameMatches`\n*  `description`, `descriptionContains`, `descriptionMatches`, `descriptionStartsWith`\n*  `checkable`, `checked`, `clickable`, `longClickable`\n*  `scrollable`, `enabled`,`focusable`, `focused`, `selected`\n*  `packageName`, `packageNameMatches`\n*  `resourceId`, `resourceIdMatches`\n*  `index`, `instance`\n\n#### Children and siblings\n\n* children\n\n  ```python\n  # get the children or grandchildren\n  d(className=\"android.widget.ListView\").child(text=\"Bluetooth\")\n  ```\n\n* siblings\n\n  ```python\n  # get siblings\n  d(text=\"Google\").sibling(className=\"android.widget.ImageView\")\n  ```\n\n* children by text or description or instance\n\n  ```python\n  # get the child matching the condition className=\"android.widget.LinearLayout\"\n  # and also its children or grandchildren with text \"Bluetooth\"\n  d(className=\"android.widget.ListView\", resourceId=\"android:id/list\") \\\n   .child_by_text(\"Bluetooth\", className=\"android.widget.LinearLayout\")\n\n  # get children by allowing scroll search\n  d(className=\"android.widget.ListView\", resourceId=\"android:id/list\") \\\n   .child_by_text(\n      \"Bluetooth\",\n      allow_scroll_search=True, # default False\n      className=\"android.widget.LinearLayout\"\n    )\n  ```\n\n  - `child_by_description` is to find children whose grandchildren have\n      the specified description, other parameters being similar to `child_by_text`.\n\n  - `child_by_instance` is to find children which have a child UI element anywhere\n      within its sub-hierarchy that is at the instance specified. It is performed\n      on visible views **without scrolling**.\n\n  See below links for detailed information:\n\n  -   [UiScrollable](http://developer.android.com/tools/help/uiautomator/UiScrollable.html), `getChildByDescription`, `getChildByText`, `getChildByInstance`\n  -   [UiCollection](http://developer.android.com/tools/help/uiautomator/UiCollection.html), `getChildByDescription`, `getChildByText`, `getChildByInstance`\n\n  Above methods support chained invoking, e.g. for the below hierarchy:\n\n  ```xml\n  <node index=\"0\" text=\"\" resource-id=\"android:id/list\" class=\"android.widget.ListView\" ...>\n    <node index=\"0\" text=\"WIRELESS & NETWORKS\" resource-id=\"\" class=\"android.widget.TextView\" .../>\n    <node index=\"1\" text=\"\" resource-id=\"\" class=\"android.widget.LinearLayout\" ...>\n      <node index=\"1\" text=\"\" resource-id=\"\" class=\"android.widget.RelativeLayout\" ...>\n        <node index=\"0\" text=\"Wi‑Fi\" resource-id=\"android:id/title\" class=\"android.widget.TextView\" .../>\n      </node>\n      <node index=\"2\" text=\"ON\" resource-id=\"com.android.settings:id/switchWidget\" class=\"android.widget.Switch\" .../>\n    </node>\n    ...\n  </node>\n  ```\n  ![settings](https://raw.github.com/xiaocong/uiautomator/master/docs/img/settings.png)\n\n  To click the switch widget right to the TextView 'Wi‑Fi', we need to select the switch widget first. However, according to the UI hierarchy, more than one switch widget exists and has almost the same properties. Selecting by className will not work. Alternatively, the below selecting strategy would help:\n\n  ```python\n  d(className=\"android.widget.ListView\", resourceId=\"android:id/list\") \\\n    .child_by_text(\"Wi‑Fi\", className=\"android.widget.LinearLayout\") \\\n    .child(className=\"android.widget.Switch\") \\\n    .click()\n  ```\n\n* relative positioning\n\n  Also, we can use the relative positioning methods to get the view: `left`, `right`, `up`, `down`.\n\n  -   `d(A).left(B)`, selects B on the left side of A.\n  -   `d(A).right(B)`, selects B on the right side of A.\n  -   `d(A).up(B)`, selects B above A.\n  -   `d(A).down(B)`, selects B under A.\n\n  So for the above cases, we can alternatively select it with:\n\n  ```python\n  ## select \"switch\" on the right side of \"Wi‑Fi\"\n  d(text=\"Wi‑Fi\").right(className=\"android.widget.Switch\").click()\n  ```\n\n* Multiple instances\n\n  Sometimes the screen may contain multiple views with the same properties, e.g. text. Then you will\n  have to use the \"instance\" property in the selector to pick one of the qualifying instances, like below:\n\n  ```python\n  d(text=\"Add new\", instance=0)  # which means the first instance with text \"Add new\"\n  ```\n\n  In addition, uiautomator2 provides a list-like API (similar to jQuery):\n\n  ```python\n  # get the count of views with text \"Add new\" on current screen\n  print(d(text=\"Add new\").count)\n\n  # same as count property\n  print(len(d(text=\"Add new\")))\n\n  # get the instance via index\n  obj = d(text=\"Add new\")[0]\n  obj = d(text=\"Add new\")[1]\n  # ...\n\n  # iterator\n  for view in d(text=\"Add new\"):\n      print(view.info)  # ...\n  ```\n\n  **Notes**: when using selectors in a code block that walks through the result list, you must ensure that the UI elements on the screen\n  remain unchanged. Otherwise, an Element-Not-Found error could occur when iterating through the list.\n\n#### Get the selected UI object status and its information\n* Check if the specific UI object exists\n\n    ```python\n    if d(text=\"Settings\").exists: # True if exists, else False\n        print(\"Settings button exists\")\n    \n    # alias of above property.\n    if d.exists(text=\"Settings\"):\n        print(\"Settings button exists\")\n\n    # advanced usage\n    if d(text=\"Settings\").exists(timeout=3): # wait for Settings to appear in 3s, same as .wait(3)\n        print(\"Settings button appeared within 3 seconds\")\n    ```\n\n* Retrieve the info of the specific UI object\n\n    ```python\n    info = d(text=\"Settings\").info\n    print(info)\n    ```\n\n    Below is a possible output:\n\n    ```json\n    {\n        \"contentDescription\": \"\",\n        \"checked\": false,\n        \"scrollable\": false,\n        \"text\": \"Settings\",\n        \"packageName\": \"com.android.launcher\",\n        \"selected\": false,\n        \"enabled\": true,\n        \"bounds\": {\n            \"top\": 385,\n            \"right\": 360,\n            \"bottom\": 585,\n            \"left\": 200\n        },\n        \"className\": \"android.widget.TextView\",\n        \"focused\": false,\n        \"focusable\": true,\n        \"clickable\": true,\n        \"childCount\": 0,\n        \"longClickable\": true,\n        \"visibleBounds\": {\n            \"top\": 385,\n            \"right\": 360,\n            \"bottom\": 585,\n            \"left\": 200\n        },\n        \"checkable\": false\n    }\n    ```\n\n* Get/Set/Clear text of an editable field (e.g., EditText widgets)\n\n    ```python\n    text_content = d(className=\"android.widget.EditText\").get_text()  # get widget text\n    d(className=\"android.widget.EditText\").set_text(\"My text...\")  # set the text\n    d(className=\"android.widget.EditText\").clear_text()  # clear the text\n    ```\n\n* Get Widget center point\n\n    ```python\n    x, y = d(text=\"Settings\").center()\n    # x, y = d(text=\"Settings\").center(offset=(0, 0)) # left-top x, y\n    ```\n    \n* Take screenshot of widget\n\n    ```python\n    im = d(text=\"Settings\").screenshot()\n    im.save(\"settings.jpg\")\n    ```\n\n#### Perform the click action on the selected UI object\n* Perform click on the specific object\n\n    ```python\n    # click on the center of the specific ui object\n    d(text=\"Settings\").click()\n    \n    # wait for element to appear for at most 10 seconds and then click\n    d(text=\"Settings\").click(timeout=10)\n    \n    # click with offset(x_offset_ratio, y_offset_ratio) from top-left of the element\n    # click_x = x_offset_ratio * width + x_left_top\n    # click_y = y_offset_ratio * height + y_left_top\n    d(text=\"Settings\").click(offset=(0.5, 0.5)) # Default: center\n    d(text=\"Settings\").click(offset=(0, 0)) # click left-top\n    d(text=\"Settings\").click(offset=(1, 1)) # click right-bottom\n\n    # click if exists within 10s, default timeout 0s\n    clicked = d(text='Skip').click_exists(timeout=10.0) # returns bool\n    \n    # click until element is gone, return bool\n    is_gone = d(text=\"Skip\").click_gone(maxretry=10, interval=1.0) # maxretry default 10, interval default 1.0s\n    ```\n\n* Perform long click on the specific UI object\n\n    ```python\n    # long click on the center of the specific UI object\n    d(text=\"Settings\").long_click()\n    # long click with duration\n    d(text=\"Settings\").long_click(duration=1.0) # duration in seconds, default 0.5s\n    ```\n\n#### Gesture actions for the specific UI object\n* Drag the UI object towards another point or another UI object \n\n    ```python\n    # notes : drag cannot be used for Android<4.3.\n    # drag the UI object to a screen point (x, y), in 0.5 seconds\n    d(text=\"Settings\").drag_to(x, y, duration=0.5)\n    # drag the UI object to (the center position of) another UI object, in 0.25 seconds\n    d(text=\"Settings\").drag_to(text=\"Clock\", duration=0.25)\n    ```\n\n* Swipe from the center of the UI object to its edge\n\n    Swipe supports 4 directions:\n\n    - `left`\n    - `right`\n    - `up` (Previously 'top')\n    - `down` (Previously 'bottom')\n\n    ```python\n    d(text=\"Settings\").swipe(\"right\")\n    d(text=\"Settings\").swipe(\"left\", steps=10) # steps control smoothness/speed\n    d(text=\"Settings\").swipe(\"up\", steps=20) # 1 step is about 5ms, so 20 steps is about 0.1s\n    d(text=\"Settings\").swipe(\"down\", steps=20)\n    ```\n\n* Two-point gesture from one pair of points to another (for pinch/zoom)\n\n  ```python\n  # ((start_x1, start_y1), (start_x2, start_y2)) are initial touch points\n  # ((end_x1, end_y1), (end_x2, end_y2)) are final touch points\n  # steps is the number of move steps to take\n  d(text=\"Settings\").gesture((sx1, sy1), (sx2, sy2), (ex1, ey1), (ex2, ey2), steps=100)\n  ```\n\n* Two-point gesture on the specific UI object (pinch in/out)\n\n  Supports two gestures:\n  - `in`: from edge to center (pinch in)\n  - `out`: from center to edge (pinch out)\n\n  ```python\n  # notes : pinch cannot be set until Android 4.3.\n  # from edge to center.\n  d(text=\"Settings\").pinch_in(percent=100, steps=10) # percent of object size, steps for smoothness\n  # from center to edge\n  d(text=\"Settings\").pinch_out(percent=100, steps=10)\n  ```\n\n* Wait until the specific UI appears or disappears\n    \n    ```python\n    # wait until the ui object appears\n    appeared = d(text=\"Settings\").wait(timeout=3.0) # return bool\n    if appeared:\n        print(\"Settings appeared\")\n    \n    # wait until the ui object is gone\n    gone = d(text=\"Settings\").wait_gone(timeout=1.0) # return bool\n    if gone:\n        print(\"Settings disappeared\")\n    ```\n\n    The default timeout is 20s. See **Global Settings** for more details.\n\n* Perform fling on the specific UI object (scrollable)\n\n  Possible properties:\n  - `horizontal` or `vertical` (or `horiz`, `vert`)\n  - `forward` or `backward` or `toBeginning` or `toEnd`\n\n  ```python\n  # fling forward(default) vertically(default) \n  d(scrollable=True).fling()\n  # fling forward horizontally\n  d(scrollable=True).fling.horizontal.forward()\n  # fling backward vertically\n  d(scrollable=True).fling.vertical.backward()\n  # fling to beginning horizontally\n  d(scrollable=True).fling.horizontal.toBeginning(max_swipes=1000)\n  # fling to end vertically\n  d(scrollable=True).fling.vertical.toEnd()\n  ```\n\n* Perform scroll on the specific UI object (scrollable)\n\n  Possible properties:\n  - `horizontal` or `vertical` (or `horiz`, `vert`)\n  - `forward` or `backward` or `toBeginning` or `toEnd`, or `to(selector)`\n\n  ```python\n  # scroll forward(default) vertically(default)\n  d(scrollable=True).scroll(steps=10)\n  # scroll forward horizontally\n  d(scrollable=True).scroll.horizontal.forward(steps=100)\n  # scroll backward vertically\n  d(scrollable=True).scroll.vertical.backward()\n  # scroll to beginning horizontally\n  d(scrollable=True).scroll.horizontal.toBeginning(steps=100, max_swipes=1000)\n  # scroll to end vertically\n  d(scrollable=True).scroll.vertical.toEnd()\n  # scroll forward vertically until specific ui object appears\n  d(scrollable=True).scroll.vertical.to(text=\"Security\")\n  ```\n\n### Input Method (IME)\n\n> IME APK: https://github.com/openatx/android-uiautomator-server/releases (Install this for reliable text input)\n\n```python\nd.send_keys(\"Hello123abcEFG\") # Send text\nd.send_keys(\"Hello123abcEFG\", clear=True) # Clear existing text then send\n\nd.clear_text() # Clear all content in the input field\n\n# Automatically performs Enter, Search, etc., based on input field requirements. Added in version 3.1\nd.send_action() \n# Can also specify the IME action, e.g., d.send_action(\"search\"). Supports go, search, send, next, done, previous.\n\nd.hide_keyboard() # Hide the soft keyboard\n```\n\nWhen `send_keys` is used, it prioritizes using the clipboard for input. If the clipboard interface is unavailable, it will attempt to install and use an auxiliary IME.\n\n```python\nprint(d.current_ime()) # Get current IME ID (package/class)\n```\n\n> For more, refer to: [IME_ACTION_CODE](https://developer.android.com/reference/android/view/inputmethod/EditorInfo)\n\n### Toast\n```python\nlast_toast_message = d.toast.get_message(wait_timeout=5, default=None) # Get last toast message text within 5s\nprint(last_toast_message)\nd.toast.reset() # Clear last toast message cache\n# d.toast.show(\"Hello\", duration=3) # Show a toast (requires special permissions)\n```\n\n### WatchContext (Deprecated)\nNote: This interface is not highly recommended. It's better to check for pop-ups before clicking elements.\n\nThe current `watch_context` uses threading and checks every 2 seconds.\nCurrently, only `click` is a trigger operation.\n\n```python\nwith d.watch_context() as ctx:\n    # When \"Download Now\" or \"Update Now\" and \"Cancel\" buttons appear simultaneously, click \"Cancel\"\n    ctx.when(\"^(立即下载|立即更新)$\").when(\"取消\").click()\n    ctx.when(\"同意\").click()\n    ctx.when(\"确定\").click()\n    # The above three lines execute immediately without waiting.\n    \n    ctx.wait_stable() # Start pop-up monitoring and wait for the interface to stabilize (stable if no pop-ups in two check cycles)\n\n    # Use the call function to trigger a callback\n    # call supports two parameters, d and el, order doesn't matter, can be omitted. If passed, variable names must be correct.\n    # e.g., When an element matching \"Midsummer Night\" appears, click the back button\n    ctx.when(\"仲夏之夜\").call(lambda d: d.press(\"back\"))\n    ctx.when(\"确定\").call(lambda el: el.click())\n\n    # Other operations\n\n# For convenience, you can also use the default pop-up monitoring logic in the code\n# Below is the current built-in default logic. You can join the group and @ the owner to add new logic, or submit a PR directly.\n    # when(\"继续使用\").click()\n    # when(\"移入管控\").when(\"取消\").click()\n    # when(\"^(立即下载|立即更新)$\").when(\"取消\").click()\n    # when(\"同意\").click()\n    # when(\"^(好的|确定)$\").click()\nwith d.watch_context(builtin=True) as ctx:\n    # Add on top of existing logic\n    ctx.when(\"@tb:id/jview_view\").when('//*[@content-desc=\"图片\"]').click()\n\n    # Other script logic\n```\n\nAlternative way:\n\n```python\nctx = d.watch_context()\nctx.when(\"设置\").click()\nctx.wait_stable() # Wait until the interface no longer has pop-ups\n\nctx.start() # if not using with statement\n# ... do something ...\nctx.stop() # or ctx.close()\n```\n\n### Global Settings\n\n```python\nimport uiautomator2 as u2\nu2.settings['HTTP_TIMEOUT'] = 60 # Default 60s, http default request timeout\n```\n\nOther configurations are mostly centralized in `d.settings`. Configurations may be added or removed based on future needs.\n\n```python\nprint(d.settings)\n# Output example:\n# {'operation_delay': (0, 0), # (before_op_delay, after_op_delay) in seconds\n#  'operation_delay_methods': ['click', 'swipe'], # methods to apply delay\n#  'wait_timeout': 20.0, # default element wait timeout (native operations, xpath plugin wait time)\n#  'xpath_debug': False, # enable xpath debug\n#  'xpath_timeout': 10.0 # default xpath wait timeout\n# }\n\n\n# Configure 0.5s delay before click, 1s delay after click\nd.settings['operation_delay'] = (0.5, 1)\n\n# Modify methods affected by delay\n# double_click, long_click correspond to 'click'\nd.settings['operation_delay_methods'] = ['click', 'swipe', 'drag', 'press']\nd.settings['wait_timeout'] = 20.0 # Default control wait time\n\nd.settings['max_depth'] = 50 # Default 50, limits dump_hierarchy returned element depth\n```\n\nWhen settings are deprecated due to version upgrades, a DeprecatedWarning will be shown, but no exception will be raised.\n\n```python\n>>> d.settings['click_before_delay'] = 1\n# [W 200514 14:55:59 settings:72] d.settings['click_before_delay'] is deprecated: Use operation_delay instead\n```\n\nUiAutomator timeout settings (hidden methods):\n\n```python\n>>> d.jsonrpc.setConfigurator({\"waitForIdleTimeout\": 100, \"waitForSelectorTimeout\": 0})\n# Check current configurator settings\n>>> print(d.jsonrpc.getConfigurator())\n# {'actionAcknowledgmentTimeout': 3000, \n#  'keyInjectionDelay': 0, \n#  'scrollAcknowledgmentTimeout': 200, \n#  'waitForIdleTimeout': 100, \n#  'waitForSelectorTimeout': 0}\n```\n\nTo prevent client program timeouts, `waitForIdleTimeout` and `waitForSelectorTimeout` are currently set to `0` by default by uiautomator2 itself (not by the underlying uiautomator server).\n\nRefs: [Google uiautomator Configurator](https://developer.android.com/reference/android/support/test/uiautomator/Configurator)\n\n## Application Management\nThis part showcases how to perform app management.\n\n### Install Application\nWe only support installing an APK from a URL or local path.\n\n```python\n# From URL\nd.app_install('http://some-domain.com/some.apk')\n\n# From local path\n# d.app_install('/path/to/your/app.apk') # This functionality might depend on adbutils or direct adb calls.\n# For local path, usually you'd use adbutils:\n# adb = adbutils.AdbClient(host=\"127.0.0.1\", port=5037)\n# device = adb.device(serial=d.serial)\n# device.install(\"/path/to/your/app.apk\")\n# Or ensure atx-agent is installed and use its features if available.\n# The simplest way with uiautomator2 if atx-agent is present:\n# d.shell(['pm', 'install', '/path/to/app.apk']) # if apk is already on device\n# d.push('/local/path/app.apk', '/data/local/tmp/app.apk')\n# d.shell(['pm', 'install', '/data/local/tmp/app.apk'])\n```\n\n### Start Application\n```python\n# Default method: first parses APK's mainActivity via atx-agent, then calls am start -n $package/$activity\nd.app_start(\"com.example.hello_world\")\n\n# Use monkey -p com.example.hello_world -c android.intent.category.LAUNCHER 1 to start\n# This method has a side effect: it automatically turns off the phone's rotation lock.\nd.app_start(\"com.example.hello_world\", use_monkey=True) # start with package name\n\n# Start app by specifying main activity, equivalent to calling am start -n com.example.hello_world/.MainActivity\nd.app_start(\"com.example.hello_world\", \".MainActivity\")\n\n# Stop app before starting\nd.app_start(\"com.example.hello_world\", stop=True)\n```\n\n### Stop Application\n\n```python\n# equivalent to `am force-stop`, thus you could lose data\nd.app_stop(\"com.example.hello_world\")\n# equivalent to `pm clear` (clears app data)\nd.app_clear('com.example.hello_world')\n```\n\n### Stop All Applications\n```python\n# stop all\nd.app_stop_all()\n# stop all apps except for com.examples.demo\nd.app_stop_all(excludes=['com.examples.demo'])\n```\n\n### Get Application Information\n```python\ninfo = d.app_info(\"com.example.demo\")\nprint(info)\n# expect output\n# {\n#    \"mainActivity\": \"com.github.uiautomator.MainActivity\",\n#    \"label\": \"ATX\",\n#    \"versionName\": \"1.1.7\",\n#    \"versionCode\": 1001007,\n#    \"size\": 1760809 # size in bytes\n# }\n\n# save app icon\nimg = d.app_icon(\"com.example.demo\") # Returns a PIL.Image object\nif img:\n    img.save(\"icon.png\")\n```\n\n### List All Running Applications\n```python\nrunning_apps = d.app_list_running()\nprint(running_apps)\n# expect output\n# [\"com.xxxx.xxxx\", \"com.github.uiautomator\", \"xxxx\"]\n```\n\n### Wait for Application to Run\n```python\npid = d.app_wait(\"com.example.android\") # Wait for app to run, returns pid (int) or 0 if timeout\nif not pid:\n    print(\"com.example.android is not running\")\nelse:\n    print(f\"com.example.android pid is {pid}\")\n\n# Wait for app to be in the foreground\npid = d.app_wait(\"com.example.android\", front=True)\nif pid:\n    print(\"com.example.android is in foreground\")\n\n# Set custom timeout (default 20.0 seconds)\npid = d.app_wait(\"com.example.android\", timeout=10.0)\n```\n\n### Push and Pull Files\n* Push a file to the device\n\n    ```python\n    # push to a folder (src can be local path or BytesIO)\n    d.push(\"foo.txt\", \"/sdcard/\") # Pushes foo.txt to /sdcard/foo.txt\n    # push and rename\n    d.push(\"foo.txt\", \"/sdcard/bar.txt\")\n    # push fileobj\n    import io\n    with io.BytesIO(b\"file content\") as f:\n        d.push(f, \"/sdcard/from_io.txt\")\n    # push and change file access mode (mode is int, e.g., 0o755)\n    d.push(\"foo.sh\", \"/data/local/tmp/\", mode=0o755) # Pushes to /data/local/tmp/foo.sh\n    ```\n\n* Pull a file from the device\n\n    ```python\n    # pull /sdcard/tmp.txt to local file tmp.txt\n    d.pull(\"/sdcard/tmp.txt\", \"tmp.txt\")\n\n    # FileNotFoundError will raise if the file is not found on the device\n    try:\n        d.pull(\"/sdcard/some-file-not-exists.txt\", \"tmp.txt\")\n    except FileNotFoundError:\n        print(\"File not found on device\")\n    \n    # Pull file content as bytes\n    # content_bytes = d.pull(\"/sdcard/tmp.txt\") # This is not a standard feature, use sync.read_bytes for this\n    # For reading content directly, use the sync object:\n    # content = d.sync.read_bytes(\"/sdcard/tmp.txt\")\n    ```\n\n### Other Application Operations\n\n```python\n# Grant all runtime permissions (requires Android 6.0+ and atx-agent)\n# d.app_auto_grant_permissions(\"io.appium.android.apis\") # This might be an older or specific helper\n\n# A more common way to grant permissions is via adb shell:\n# d.shell(['pm', 'grant', 'io.appium.android.apis', 'android.permission.READ_CONTACTS'])\n\n# Open URL scheme\nd.open_url(\"appname://appnamehost\")\n# same as\n# adb shell am start -a android.intent.action.VIEW -d \"appname://appnamehost\"\n```\n\n### Session (Beta)\nSession represents an app lifecycle. Can be used to start app, detect app crash.\n\n* Launch and close app\n\n    ```python\n    sess = d.session(\"com.netease.cloudmusic\") # Starts NetEase Cloud Music\n    # ... perform operations within the session context ...\n    # sess(text=\"Play\").click()\n    sess.close() # Stops NetEase Cloud Music\n    # sess.restart() # Cold starts NetEase Cloud Music (stops then starts)\n    ```\n\n* Use python `with` to launch and close app\n\n    ```python\n    with d.session(\"com.netease.cloudmusic\") as sess:\n        # sess(text=\"Play\").click()\n        # App will be closed automatically when exiting the 'with' block\n        pass\n    ```\n\n* Attach to the running app\n\n    ```python\n    # Launch app if not running, skip launch if already running\n    sess = d.session(\"com.netease.cloudmusic\", attach=True)\n    ```\n\n* Detect app crash\n\n    ```python\n    # When app is still running\n    # sess(text=\"Music\").click() # operation goes normal\n\n    # If app crashes or quits\n    # sess(text=\"Music\").click() # raises SessionBrokenError\n    # other function calls under session will raise SessionBrokenError too\n    ```\n\n    ```python\n    # check if session is ok.\n    # Warning: function name may change in the future\n    if sess.running(): # True or False\n        print(\"Session is active\")\n    else:\n        print(\"Session is not active (app might have crashed or closed)\")\n    ```\n\n\n## Other APIs\n\n### Stop Background HTTP Service\nNormally, when the Python program exits, the UiAutomator service on the device also exits.\nHowever, you can also stop the service via an API call.\n\n```python\nd.uiautomator.stop() # Stops the uiautomator service on the device\n# or d.service(\"uiautomator\").stop()\n```\n\n### Enable Debugging\nPrint out the HTTP request information behind the code.\n\n```python\n>>> d.debug = True # This enables logging for uiautomator2 library\n>>> print(d.info)\n# Example output showing HTTP request/response\n# 12:32:47.182 $ curl -X POST -d '{\"jsonrpc\": \"2.0\", \"id\": \"...\", \"method\": \"deviceInfo\"}' 'http://127.0.0.1:PORT/jsonrpc/0'\n# 12:32:47.225 Response >>>\n# {\"jsonrpc\":\"2.0\",\"id\":\"...\",\"result\":{...}}\n# <<< END\n```\n\nFor more structured logging:\n```python\nimport logging\nfrom uiautomator2 import set_log_level\n\nset_log_level(logging.DEBUG) # or logging.INFO\n\n# Or configure manually\n# logger = logging.getLogger(\"uiautomator2\")\n# logger.setLevel(logging.DEBUG)\n# # setup handler, formatter etc.\n```\n\n## Command Line Functions\n`$device_ip` represents the device's IP address.\n\nTo specify a device, pass `--serial`, e.g., `python -m uiautomator2 --serial bff1234 <SubCommand>`. SubCommand can be `screenshot`, `current`, etc.\n\n> 1.0.3 Added: `python -m uiautomator2` is equivalent to `uiautomator2`\n\n- `screenshot`: Take a screenshot\n\n    ```bash\n    uiautomator2 screenshot screenshot.jpg\n    # With specific device\n    uiautomator2 --serial <YOUR_DEVICE_SERIAL> screenshot screenshot.jpg\n    ```\n\n- `copy-assets`: Copy assets/ to current directory\n\n    Used for pyinstaller、nuitka\n\n- `current`: Get current package name and activity\n\n    ```bash\n    uiautomator2 current\n    # Output example:\n    # {\n    #     \"package\": \"com.android.settings\",\n    #     \"activity\": \".Settings\",\n    #     \"pid\": 12345 \n    # }\n    ```\n    \n- `uninstall`: Uninstall app\n\n    ```bash\n    uiautomator2 uninstall <package-name> # Uninstall one package\n    uiautomator2 uninstall <package-name-1> <package-name-2> # Uninstall multiple packages\n    # uiautomator2 uninstall --all # Uninstall all third-party apps (Be careful!)\n    ```\n\n- `stop`: Stop app\n\n    ```bash\n    uiautomator2 stop com.example.app # Stop one app\n    # uiautomator2 stop --all # Stop all apps (Be careful!)\n    ```\n\n- `doctor`: Check uiautomator2 environment\n\n    ```bash\n    uiautomator2 doctor\n    # Example output:\n    # [I 2024-04-25 19:53:36,288 __main__:101 pid:15596] uiautomator2 is OK\n    ```\n- `install`: Install APK from URL or local path\n    ```bash\n    uiautomator2 install http://example.com/app.apk\n    uiautomator2 install /path/to/local/app.apk\n    ```\n- `clear`: Clear app data\n    ```bash\n    uiautomator2 clear <package-name>\n    ```\n- `start`: Start app\n    ```bash\n    uiautomator2 start <package-name>\n    uiautomator2 start <package-name>/<activity-name>\n    ```\n- `version`: Show uiautomator2 version\n    ```bash\n    uiautomator2 version\n    ```\n\n## Differences between Google UiAutomator 2.0 and 1.x\nReference: https://www.cnblogs.com/insist8089/p/6898181.html (Chinese)\n\n- **New APIs**: UiObject2, Until, By, BySelector (in UiAutomator 2.x Java library)\n- **Import Style**: In 2.0, `com.android.uiautomator.core.*` is deprecated. Use `android.support.test.uiautomator.*` (now `androidx.test.uiautomator.*`).\n- **Build System**: Maven and/or Ant (1.x); Gradle (2.0).\n- **Test Package Format**: From zip/jar (1.x) to APK (2.0).\n- **Running Tests via ADB**:\n  - 1.x: `adb shell uiautomator runtest UiTest.jar -c package.name.ClassName`\n  - 2.0: `adb shell am instrument -w -r -e debug false -e class package.name.ClassName#methodName package.name.test/androidx.test.runner.AndroidJUnitRunner`\n- **Access to Android Services/APIs**: 1.x: No; 2.0: Yes.\n- **Log Output**: 1.x: `System.out.print` echoes to the execution terminal; 2.0: Output to Logcat.\n- **Execution**: 2.0: Test cases do not need to inherit from any parent class, method names are not restricted, uses Annotations; 1.x: Needs to inherit `UiAutomatorTestCase`, test methods must start with `test`.\n\n(Note: uiautomator2 Python library abstracts away many of these Java-level differences, but understanding the underlying UiAutomator evolution can be helpful.)\n\n## Dependent Projects\n- uiautomator-jsonrpc-server: <https://github.com/openatx/android-uiautomator-server/> (The core server running on the Android device)\n- adbutils: <https://github.com/openatx/adbutils> (For ADB communication)\n\n# Contributors\n\n[contributors](../../graphs/contributors)\n\n# Other Excellent Projects\n\n- <https://github.com/ecnusse/Kea2>: Fusing automated UI testing with scripts for effectively fuzzing Android apps.\n- <https://github.com/atinfo/awesome-test-automation>: A collection of excellent test automation frameworks.\n- [google/mobly](https://github.com/google/mobly): Google's internal test framework.\n- <https://github.com/zhangzhao4444/Maxim>: A monkey test tool based on UiAutomator.\n- <http://www.sikulix.com/>: A well-established image-based automation framework.\n- <http://airtest.netease.com/>: The predecessor of this project, later taken over and optimized by NetEase Guangzhou team. Features a good IDE. (archived)\n\n(Order matters, additions welcome)\n\n# LICENSE\n[MIT](LICENSE)\n"
  },
  {
    "path": "README_CN.md",
    "content": "# uiautomator2\n\n[![PyPI](https://img.shields.io/pypi/v/uiautomator2.svg)](https://pypi.python.org/pypi/uiautomator2)\n![PyPI](https://img.shields.io/pypi/pyversions/uiautomator2.svg)\n[![codecov](https://codecov.io/gh/openatx/uiautomator2/graph/badge.svg?token=d0ZLkqorBu)](https://codecov.io/gh/openatx/uiautomator2)\n\n[📖 Read the English version](README.md)\n\n一个简单、好用、稳定的Android自动化的库\n\n## 工作原理\n本框架主要包含两个部分:\n\n1. 手机端: 运行一个基于UiAutomator的HTTP服务，提供Android自动化的各种接口\n2. Python客户端: 通过HTTP协议与手机端通信，调用UiAutomator的各种功能\n\n简单来说就是把Android自动化的能力通过HTTP接口的方式暴露给Python使用。这种设计使得Python端的代码编写更加简单直观。\n\n> 还在用2.x.x版本的用户，可以先看一下[2to3](docs/2to3.md) 再决定是否要升级3.x.x （强烈建议升级）\n\n## 交流群\n\n- QQ交流群: 1群:815453846 2群:943964182\n- Discord: <https://discord.gg/PbJhnZJKDd>\n\n# 依赖\n- Android版本 4.4+\n- Python 3.8+\n\n# 安装\n\n```sh\npip install uiautomator2\n\n# 检查是否安装成功，正常情况下会输出库的版本好\nuiautomator2 version\n# or: python -m uiautomator2 version\n```\n\n安装元素查看工具（可选，但是强烈推荐）\n\n> 更详细的使用说明参考: https://github.com/codeskyblue/uiautodev QQ:536481989\n\n\n```sh\npip install uiautodev\n\n# 命令行启动后会自动打开浏览器\nuiautodev\n# or: python -m uiautodev\n```\n\n代替品: uiautomatorviewer, Appium Inspector\n\n# 快速入门\n\n准备一台开启了`开发者选项`的安卓手机，连接上电脑，确保执行`adb devices`可以看到连接上的设备。\n\n打开python交互窗口。然后将下面的命令输入到窗口中。\n\n```python\nimport uiautomator2 as u2\n\nd = u2.connect() # 连接多台设备需要指定设备序列号\nprint(d.info)\n# 期望输出\n# {'currentPackageName': 'net.oneplus.launcher', 'displayHeight': 1920, 'displayRotation': 0, 'displaySizeDpX': 411, 'displaySizeDpY': 731, 'displayWidth': 1080, 'productName': 'OnePlus5', 'screenOn': True, 'sdkInt': 27, 'naturalOrientation': True}\n```\n\n脚本例子\n\n```python\nimport uiautomator2 as u2\n\nd = u2.connect('Q5S5T19611004599')\nd.app_start('tv.danmaku.bili', stop=True) # 启动Bilibili\nd.wait_activity('.MainActivityV2')\nd.sleep(5) # 等待开屏广告消失\nd.xpath('//*[@text=\"我的\"]').click()\n# 获取粉丝数量\nfans_count = d.xpath('//*[@resource-id=\"tv.danmaku.bili:id/fans_count\"]').text\nprint(f\"粉丝数量: {fans_count}\")\n```\n\n# 使用文档\n\n## 连接设备\n\n方法1: 使用设备序列号链接设备 例如序列号. `Q5S5T19611004599` (seen from `adb devices`)\n\n```python\nimport uiautomator2 as u2\n\nd = u2.connect('Q5S5T19611004599') # alias for u2.connect_usb('123456f')\nprint(d.info)\n```\n\n方法2: 序列号可以通过环境变量传递 `ANDROID_SERIAL`\n\n\n```python\n# export ANDROID_SERIAL=Q5S5T19611004599\nd = u2.connect()\n```\n\n方法3: 通过transport_id指定设备\n\n```sh\n$ adb devices -l\nQ5S5T19611004599       device 0-1.2.2 product:ELE-AL00 model:ELE_AL00 device:HWELE transport_id:6\n```\n\n这里可以看到transport_id:6\n\n> 也可以通过 adbutils.adb.list(extended=True)获取所有连接的transport_id\n> 参考 https://github.com/openatx/adbutils\n\n```python\nimport adbutils # 需要版本>=2.9.1\nimport uiautomator2 as u2\ndev = adbutils.device(transport_id=6)\nd = u2.connect(dev)\n```\n\n## 通过XPath操作元素\n\n什么是XPath：\n\nXPath 是一种在 XML 或 HTML 文档中定位内容的查询语言。它使用简单的语法规则建立从根节点到所需元素的路径。\n\n基本语法：\n- `/` - 从根节点开始选择\n- `//` - 从当前节点开始选择任意位置\n- `.` - 选择当前节点 \n- `..` - 选择当前节点的父节点\n- `@` - 选择属性\n- `[]` - 谓语表达式，用于过滤条件\n\n通过[UIAutoDev](https://uiauto.dev)可以快速的生成XPath\n\n常用用法\n\n```python\nd.xpath('//*[@text=\"私人FM\"]').click()\n\n# 语法糖\nd.xpath('@personal-fm') # 等价于 d.xpath('//*[@resource-id=\"personal-fm\"]')\n\nsl = d.xpath(\"@com.example:id/home_searchedit\") # sl为XPathSelector对象\nsl.click()\nsl.click(timeout=10) # 指定超时时间, 找不到抛出异常 XPathElementNotFoundError\nsl.click_exists() # 存在即点击，返回是否点击成功\nsl.click_exists(timeout=10) # 等待最多10s钟\n\n# 等到对应的元素出现，返回XMLElement\n# 默认的等待时间是10s\nel = sl.wait()\nel = sl.wait(timeout=15) # 等待15s, 没有找到会返回None\n\n# 等待元素消失\nsl.wait_gone()\nsl.wait_gone(timeout=15) \n\n# 跟wait用法类似，区别是如果没找到直接抛出 XPathElementNotFoundError 异常\nel = sl.get() \nel = sl.get(timeout=15)\n\nsl.get_text() # 获取组件名\nsl.set_text(\"\") # 清空输入框\nsl.set_text(\"hello world\") # 输入框输入 hello world\n```\n\n更多用法参考 [XPath接口文档](XPATH_CN.md)\n\n## 插件\n\n- webview: https://github.com/YuYoungG/uiautomator2-webview\n\n为了保持项目的简洁与可扩展性，后续插件将以第三方库的形式接入。\n\n## 通过UiAutomator接口操作元素\n\n### 元素等待时长\n设置元素查找等待时间（默认20s）\n\n```python\nd.implicitly_wait(10.0) # 也可以通过d.settings['wait_timeout'] = 10.0 修改\nprint(\"wait timeout\", d.implicitly_wait()) # get default implicit wait\n\n# 如果Settings 10s没有出现就抛出异常 UiObjectNotFoundError\nd(text=\"Settings\").click() \n```\n\n等待时长影响如下函数 `click`, `long_click`, `drag_to`, `get_text`, `set_text`, `clear_text`\n \n\n### 获取设备信息\n\n通过UiAutomator获取到的信息\n\n```python\nd.info\n# Output\n{'currentPackageName': 'com.android.systemui',\n 'displayHeight': 1560,\n 'displayRotation': 0,\n 'displaySizeDpX': 360,\n 'displaySizeDpY': 780,\n 'displayWidth': 720,\n 'naturalOrientation': True,\n 'productName': 'ELE-AL00',\n 'screenOn': True,\n 'sdkInt': 29}\n```\n\n获取设备信息（基于adb shell getprop命令）\n\n```python\nprint(d.device_info)\n# output\n{'arch': 'arm64-v8a',\n 'brand': 'google',\n 'model': 'sdk_gphone64_arm64',\n 'sdk': 34,\n 'serial': 'EMULATOR34X1X19X0',\n 'version': 14}\n```\n\n获取屏幕物理尺寸 （依赖adb shell wm size)\n\n```python\nprint(d.window_size())\n# device upright output example: (1080, 1920)\n# device horizontal output example: (1920, 1080)\n```\n\n获取当前App (依赖adb shell)\n\n```python\nprint(d.app_current())\n# Output example 1: {'activity': '.Client', 'package': 'com.netease.example', 'pid': 23710}\n# Output example 2: {'activity': '.Client', 'package': 'com.netease.example'}\n# Output example 3: {'activity': None, 'package': None}\n```\n\n等待Activity （依赖adb shell）\n\n```python\nd.wait_activity(\".ApiDemos\", timeout=10) # default timeout 10.0 seconds\n# Output: true of false\n```\n\n获取设备序列号\n\n```python\nprint(d.serial)\n# output example: 74aAEDR428Z9\n```\n\n获取设备WLAN IP (依赖adb shell)\n\n```python\nprint(d.wlan_ip)\n# output example: 10.0.0.1 or None\n```\n\n### 剪贴板\n设置粘贴板内容或获取内容\n\n* clipboard/set_clipboard\n\n    ```python\n    # 设置剪贴板\n    d.clipboard = 'hello-world'\n    # or\n    d.set_clipboard('hello-world', 'label')\n\n    # 获取剪贴板\n    # 依赖输入法(com.github.uiautomator/.AdbKeyboard)\n    d.set_input_ime()\n    print(d.clipboard)\n    ```\n\n### Key Events\n\n* Turn on/off screen\n\n    ```python\n    d.screen_on() # turn on the screen\n    d.screen_off() # turn off the screen\n    ```\n\n* Get current screen status\n\n    ```python\n    d.info.get('screenOn')\n    ```\n\n* Press hard/soft key\n\n    ```python\n    d.press(\"home\") # press the home key, with key name\n    d.press(\"back\") # press the back key, with key name\n    d.press(0x07, 0x02) # press keycode 0x07('0') with META ALT(0x02)\n    ```\n\n* These key names are currently supported:\n\n    - home\n    - back\n    - left\n    - right\n    - up\n    - down\n    - center\n    - menu\n    - search\n    - enter\n    - delete ( or del)\n    - recent (recent apps)\n    - volume_up\n    - volume_down\n    - volume_mute\n    - camera\n    - power\n\nYou can find all key code definitions at [Android KeyEvnet](https://developer.android.com/reference/android/view/KeyEvent.html)\n\n* Unlock screen\n\n    ```python\n    d.unlock()\n    # This is equivalent to\n    # 1. press(\"power\")\n    # 2. swipe from left-bottom to right-top\n    ```\n\n### Gesture interaction with the device\n* Click on the screen\n\n    ```python\n    d.click(x, y)\n    ```\n\n* Double click\n\n    ```python\n    d.double_click(x, y)\n    d.double_click(x, y, 0.1) # default duration between two click is 0.1s\n    ```\n\n* Long click on the screen\n\n    ```python\n    d.long_click(x, y)\n    d.long_click(x, y, 0.5) # long click 0.5s (default)\n    ```\n\n* Swipe\n\n    ```python\n    d.swipe(sx, sy, ex, ey)\n    d.swipe(sx, sy, ex, ey, 0.5) # swipe for 0.5s(default)\n    ```\n\n* SwipeExt 扩展功能\n\n    ```python\n    d.swipe_ext(\"right\") # 手指右滑，4选1 \"left\", \"right\", \"up\", \"down\"\n    d.swipe_ext(\"right\", scale=0.9) # 默认0.9, 滑动距离为屏幕宽度的90%\n    d.swipe_ext(\"right\", box=(0, 0, 100, 100)) # 在 (0,0) -> (100, 100) 这个区域做滑动\n\n    # 实践发现上滑或下滑的时候，从中点开始滑动成功率会高一些\n    d.swipe_ext(\"up\", scale=0.8) # 代码会vkk\n\n    # 还可以使用Direction作为参数\n    from uiautomator2 import Direction\n    \n    d.swipe_ext(Direction.FORWARD) # 页面下翻, 等价于 d.swipe_ext(\"up\"), 只是更好理解\n    d.swipe_ext(Direction.BACKWARD) # 页面上翻\n    d.swipe_ext(Direction.HORIZ_FORWARD) # 页面水平右翻\n    d.swipe_ext(Direction.HORIZ_BACKWARD) # 页面水平左翻\n    ```\n\n* Drag\n\n    ```python\n    d.drag(sx, sy, ex, ey)\n    d.drag(sx, sy, ex, ey, 0.5) # swipe for 0.5s(default)\n\n* Swipe points\n\n    ```python\n    # swipe from point(x0, y0) to point(x1, y1) then to point(x2, y2)\n    # time will speed 0.2s bwtween two points\n    d.swipe_points([(x0, y0), (x1, y1), (x2, y2)], 0.2))\n    ```\n\n    多用于九宫格解锁，提前获取到每个点的相对坐标（这里支持百分比），\n    更详细的使用参考这个帖子 [使用u2实现九宫图案解锁](https://testerhome.com/topics/11034)\n\n* Touch and drap (Beta)\n\n    这个接口属于比较底层的原始接口，感觉并不完善，不过凑合能用。注：这个地方并不支持百分比\n\n    ```python\n    d.touch.down(10, 10) # 模拟按下\n    time.sleep(.01) # down 和 move 之间的延迟，自己控制\n    d.touch.move(15, 15) # 模拟移动\n    d.touch.up(10, 10) # 模拟抬起\n    ```\n\nNote: click, swipe, drag operations support percentage position values. Example:\n\n`d.long_click(0.5, 0.5)` means long click center of screen\n\n### 屏幕相关接口\n* Retrieve/Set device orientation\n\n    The possible orientations:\n\n    -   `natural` or `n`\n    -   `left` or `l`\n    -   `right` or `r`\n    -   `upsidedown` or `u` (can not be set)\n\n    ```python\n    # retrieve orientation. the output could be \"natural\" or \"left\" or \"right\" or \"upsidedown\"\n    orientation = d.orientation\n\n    # WARNING: not pass testing in my TT-M1\n    # set orientation and freeze rotation.\n    # notes: setting \"upsidedown\" requires Android>=4.3.\n    d.set_orientation('l') # or \"left\"\n    d.set_orientation(\"l\") # or \"left\"\n    d.set_orientation(\"r\") # or \"right\"\n    d.set_orientation(\"n\") # or \"natural\"\n    ```\n\n* Freeze/Un-freeze rotation\n\n    ```python\n    # freeze rotation\n    d.freeze_rotation()\n    # un-freeze rotation\n    d.freeze_rotation(False)\n    ```\n\n* Take screenshot\n\n    ```python\n    # take screenshot and save to a file on the computer, require Android>=4.2.\n    d.screenshot(\"home.jpg\")\n    \n    # get PIL.Image formatted images. Naturally, you need pillow installed first\n    image = d.screenshot() # default format=\"pillow\"\n    image.save(\"home.jpg\") # or home.png. Currently, only png and jpg are supported\n\n    # get opencv formatted images. Naturally, you need numpy and cv2 installed first\n    import cv2\n    image = d.screenshot(format='opencv')\n    cv2.imwrite('home.jpg', image)\n\n    # get raw jpeg data\n    imagebin = d.screenshot(format='raw')\n    open(\"some.jpg\", \"wb\").write(imagebin)\n    ```\n\n* Dump UI hierarchy\n\n    ```python\n    # get the UI hierarchy dump content\n    xml = d.dump_hierarchy()\n\n    # compressed=True: include not import nodes\n    # pretty: format xml\n    # max_depth: limit xml depth, default 50\n    xml = d.dump_hierarchy(compressed=False, pretty=False, max_depth=50)\n    ```\n\n* Open notification or quick settings\n\n    ```python\n    d.open_notification()\n    d.open_quick_settings()\n    ```\n\n### Selector\n\nSelector is a handy mechanism to identify a specific UI object in the current window.\n\n```python\n# Select the object with text 'Clock' and its className is 'android.widget.TextView'\nd(text='Clock', className='android.widget.TextView')\n```\n\nSelector supports below parameters. Refer to [UiSelector Java doc](http://developer.android.com/tools/help/uiautomator/UiSelector.html) for detailed information.\n\n*  `text`, `textContains`, `textMatches`, `textStartsWith`\n*  `className`, `classNameMatches`\n*  `description`, `descriptionContains`, `descriptionMatches`, `descriptionStartsWith`\n*  `checkable`, `checked`, `clickable`, `longClickable`\n*  `scrollable`, `enabled`,`focusable`, `focused`, `selected`\n*  `packageName`, `packageNameMatches`\n*  `resourceId`, `resourceIdMatches`\n*  `index`, `instance`\n\n#### Children and siblings\n\n* children\n\n  ```python\n  # get the children or grandchildren\n  d(className=\"android.widget.ListView\").child(text=\"Bluetooth\")\n  ```\n\n* siblings\n\n  ```python\n  # get siblings\n  d(text=\"Google\").sibling(className=\"android.widget.ImageView\")\n  ```\n\n* children by text or description or instance\n\n  ```python\n  # get the child matching the condition className=\"android.widget.LinearLayout\"\n  # and also its children or grandchildren with text \"Bluetooth\"\n  d(className=\"android.widget.ListView\", resourceId=\"android:id/list\") \\\n   .child_by_text(\"Bluetooth\", className=\"android.widget.LinearLayout\")\n\n  # get children by allowing scroll search\n  d(className=\"android.widget.ListView\", resourceId=\"android:id/list\") \\\n   .child_by_text(\n      \"Bluetooth\",\n      allow_scroll_search=True,\n      className=\"android.widget.LinearLayout\"\n    )\n  ```\n\n  - `child_by_description` is to find children whose grandchildren have\n      the specified description, other parameters being similar to `child_by_text`.\n\n  - `child_by_instance` is to find children with has a child UI element anywhere\n      within its sub hierarchy that is at the instance specified. It is performed\n      on visible views **without scrolling**.\n\n  See below links for detailed information:\n\n  -   [UiScrollable](http://developer.android.com/tools/help/uiautomator/UiScrollable.html), `getChildByDescription`, `getChildByText`, `getChildByInstance`\n  -   [UiCollection](http://developer.android.com/tools/help/uiautomator/UiCollection.html), `getChildByDescription`, `getChildByText`, `getChildByInstance`\n\n  Above methods support chained invoking, e.g. for below hierarchy\n\n  ```xml\n  <node index=\"0\" text=\"\" resource-id=\"android:id/list\" class=\"android.widget.ListView\" ...>\n    <node index=\"0\" text=\"WIRELESS & NETWORKS\" resource-id=\"\" class=\"android.widget.TextView\" .../>\n    <node index=\"1\" text=\"\" resource-id=\"\" class=\"android.widget.LinearLayout\" ...>\n      <node index=\"1\" text=\"\" resource-id=\"\" class=\"android.widget.RelativeLayout\" ...>\n        <node index=\"0\" text=\"Wi‑Fi\" resource-id=\"android:id/title\" class=\"android.widget.TextView\" .../>\n      </node>\n      <node index=\"2\" text=\"ON\" resource-id=\"com.android.settings:id/switchWidget\" class=\"android.widget.Switch\" .../>\n    </node>\n    ...\n  </node>\n  ```\n  ![settings](https://raw.github.com/xiaocong/uiautomator/master/docs/img/settings.png)\n\n  To click the switch widget right to the TextView 'Wi‑Fi', we need to select the switch widgets first. However, according to the UI hierarchy, more than one switch widgets exist and have almost the same properties. Selecting by className will not work. Alternatively, the below selecting strategy would help:\n\n  ```python\n  d(className=\"android.widget.ListView\", resourceId=\"android:id/list\") \\\n    .child_by_text(\"Wi‑Fi\", className=\"android.widget.LinearLayout\") \\\n    .child(className=\"android.widget.Switch\") \\\n    .click()\n  ```\n\n* relative positioning\n\n  Also we can use the relative positioning methods to get the view: `left`, `right`, `top`, `bottom`.\n\n  -   `d(A).left(B)`, selects B on the left side of A.\n  -   `d(A).right(B)`, selects B on the right side of A.\n  -   `d(A).up(B)`, selects B above A.\n  -   `d(A).down(B)`, selects B under A.\n\n  So for above cases, we can alternatively select it with:\n\n  ```python\n  ## select \"switch\" on the right side of \"Wi‑Fi\"\n  d(text=\"Wi‑Fi\").right(className=\"android.widget.Switch\").click()\n  ```\n\n* Multiple instances\n\n  Sometimes the screen may contain multiple views with the same properties, e.g. text, then you will\n  have to use the \"instance\" property in the selector to pick one of qualifying instances, like below:\n\n  ```python\n  d(text=\"Add new\", instance=0)  # which means the first instance with text \"Add new\"\n  ```\n\n  In addition, uiautomator2 provides a list-like API (similar to jQuery):\n\n  ```python\n  # get the count of views with text \"Add new\" on current screen\n  d(text=\"Add new\").count\n\n  # same as count property\n  len(d(text=\"Add new\"))\n\n  # get the instance via index\n  d(text=\"Add new\")[0]\n  d(text=\"Add new\")[1]\n  ...\n\n  # iterator\n  for view in d(text=\"Add new\"):\n      view.info  # ...\n  ```\n\n  **Notes**: when using selectors in a code block that walk through the result list, you must ensure that the UI elements on the screen\n  keep unchanged. Otherwise, when Element-Not-Found error could occur when iterating through the list.\n\n#### Get the selected ui object status and its information\n* Check if the specific UI object exists\n\n    ```python\n    d(text=\"Settings\").exists # True if exists, else False\n    d.exists(text=\"Settings\") # alias of above property.\n\n    # advanced usage\n    d(text=\"Settings\").exists(timeout=3) # wait Settings appear in 3s, same as .wait(3)\n    ```\n\n* Retrieve the info of the specific UI object\n\n    ```python\n    d(text=\"Settings\").info\n    ```\n\n    Below is a possible output:\n\n    ```\n    { u'contentDescription': u'',\n    u'checked': False,\n    u'scrollable': False,\n    u'text': u'Settings',\n    u'packageName': u'com.android.launcher',\n    u'selected': False,\n    u'enabled': True,\n    u'bounds': {u'top': 385,\n                u'right': 360,\n                u'bottom': 585,\n                u'left': 200},\n    u'className': u'android.widget.TextView',\n    u'focused': False,\n    u'focusable': True,\n    u'clickable': True,\n    u'chileCount': 0,\n    u'longClickable': True,\n    u'visibleBounds': {u'top': 385,\n                        u'right': 360,\n                        u'bottom': 585,\n                        u'left': 200},\n    u'checkable': False\n    }\n    ```\n\n* Get/Set/Clear text of an editable field (e.g., EditText widgets)\n\n    ```python\n    d(text=\"Settings\").get_text()  # get widget text\n    d(text=\"Settings\").set_text(\"My text...\")  # set the text\n    d(text=\"Settings\").clear_text()  # clear the text\n    ```\n\n* Get Widget center point\n\n    ```python\n    x, y = d(text=\"Settings\").center()\n    # x, y = d(text=\"Settings\").center(offset=(0, 0)) # left-top x, y\n    ```\n    \n* Take screenshot of widget\n\n    ```python\n    im = d(text=\"Settings\").screenshot()\n    im.save(\"settings.jpg\")\n    ```\n\n#### Perform the click action on the selected UI object\n* Perform click on the specific   object\n\n    ```python\n    # click on the center of the specific ui object\n    d(text=\"Settings\").click()\n    \n    # wait element to appear for at most 10 seconds and then click\n    d(text=\"Settings\").click(timeout=10)\n    \n    # click with offset(x_offset, y_offset)\n    # click_x = x_offset * width + x_left_top\n    # click_y = y_offset * height + y_left_top\n    d(text=\"Settings\").click(offset=(0.5, 0.5)) # Default center\n    d(text=\"Settings\").click(offset=(0, 0)) # click left-top\n    d(text=\"Settings\").click(offset=(1, 1)) # click right-bottom\n\n    # click when exists in 10s, default timeout 0s\n    clicked = d(text='Skip').click_exists(timeout=10.0)\n    \n    # click until element gone, return bool\n    is_gone = d(text=\"Skip\").click_gone(maxretry=10, interval=1.0) # maxretry default 10, interval default 1.0\n    ```\n\n* Perform long click on the specific UI object\n\n    ```python\n    # long click on the center of the specific UI object\n    d(text=\"Settings\").long_click()\n    ```\n\n#### Gesture actions for the specific UI object\n* Drag the UI object towards another point or another UI object \n\n    ```python\n    # notes : drag can not be used for Android<4.3.\n    # drag the UI object to a screen point (x, y), in 0.5 second\n    d(text=\"Settings\").drag_to(x, y, duration=0.5)\n    # drag the UI object to (the center position of) another UI object, in 0.25 second\n    d(text=\"Settings\").drag_to(text=\"Clock\", duration=0.25)\n    ```\n\n* Swipe from the center of the UI object to its edge\n\n    Swipe supports 4 directions:\n\n    - left\n    - right\n    - top\n    - bottom\n\n    ```python\n    d(text=\"Settings\").swipe(\"right\")\n    d(text=\"Settings\").swipe(\"left\", steps=10)\n    d(text=\"Settings\").swipe(\"up\", steps=20) # 1 steps is about 5ms, so 20 steps is about 0.1s\n    d(text=\"Settings\").swipe(\"down\", steps=20)\n    ```\n\n* Two-point gesture from one point to another\n\n  ```python\n  d(text=\"Settings\").gesture((sx1, sy1), (sx2, sy2), (ex1, ey1), (ex2, ey2))\n  ```\n\n* Two-point gesture on the specific UI object\n\n  Supports two gestures:\n  - `In`, from edge to center\n  - `Out`, from center to edge\n\n  ```python\n  # notes : pinch can not be set until Android 4.3.\n  # from edge to center. here is \"In\" not \"in\"\n  d(text=\"Settings\").pinch_in(percent=100, steps=10)\n  # from center to edge\n  d(text=\"Settings\").pinch_out()\n  ```\n\n* Wait until the specific UI appears or disappears\n    \n    ```python\n    # wait until the ui object appears\n    d(text=\"Settings\").wait(timeout=3.0) # return bool\n    # wait until the ui object gone\n    d(text=\"Settings\").wait_gone(timeout=1.0)\n    ```\n\n    The default timeout is 20s. see **global settings** for more details\n\n* Perform fling on the specific ui object(scrollable)\n\n  Possible properties:\n  - `horiz` or `vert`\n  - `forward` or `backward` or `toBeginning` or `toEnd`\n\n  ```python\n  # fling forward(default) vertically(default) \n  d(scrollable=True).fling()\n  # fling forward horizontally\n  d(scrollable=True).fling.horiz.forward()\n  # fling backward vertically\n  d(scrollable=True).fling.vert.backward()\n  # fling to beginning horizontally\n  d(scrollable=True).fling.horiz.toBeginning(max_swipes=1000)\n  # fling to end vertically\n  d(scrollable=True).fling.toEnd()\n  ```\n\n* Perform scroll on the specific ui object(scrollable)\n\n  Possible properties:\n  - `horiz` or `vert`\n  - `forward` or `backward` or `toBeginning` or `toEnd`, or `to`\n\n  ```python\n  # scroll forward(default) vertically(default)\n  d(scrollable=True).scroll(steps=10)\n  # scroll forward horizontally\n  d(scrollable=True).scroll.horiz.forward(steps=100)\n  # scroll backward vertically\n  d(scrollable=True).scroll.vert.backward()\n  # scroll to beginning horizontally\n  d(scrollable=True).scroll.horiz.toBeginning(steps=100, max_swipes=1000)\n  # scroll to end vertically\n  d(scrollable=True).scroll.toEnd()\n  # scroll forward vertically until specific ui object appears\n  d(scrollable=True).scroll.to(text=\"Security\")\n  ```\n\n### 输入法\n\n> 输入法APK: https://github.com/openatx/android-uiautomator-server/releases\n\n```python\nd.send_keys(\"你好123abcEFG\")\nd.send_keys(\"你好123abcEFG\", clear=True)\n\nd.clear_text() # 清除输入框所有内容\n\nd.send_action() # 根据输入框的需求，自动执行回车、搜索等指令, Added in version 3.1\n# 也可以指定发送的输入法action, eg: d.send_action(\"search\") 支持 go, search, send, next, done, previous\n\nd.hide_keyboard() # 隐藏输入法\n```\n\n输入法send_keys的时候，优先使用剪贴板进行输入。如果剪贴板接口无法使用，会安装辅助输入法进行输入。\n\n\n```python\nprint(d.current_ime()) # 获取当前输入法ID\n```\n\n> 更多参考: [IME_ACTION_CODE](https://developer.android.com/reference/android/view/inputmethod/EditorInfo)\n\n### Toast\n```python\nprint(d.last_toast) # get last toast, if not toast return None\nd.clear_toast()\n```\n\n### WatchContext (废弃)\n注: 这里不是很推荐用这个接口，最好点击元素前检查一下是否有弹窗\n\n目前的这个watch_context是用threading启动的，每2s检查一次\n目前还只有click这一种触发操作\n\n```python\nwith d.watch_context() as ctx:\n    # 当同时出现 （立即下载 或 立即更新）和 取消 按钮的时候，点击取消\n    ctx.when(\"^立即(下载|更新)\").when(\"取消\").click() \n    ctx.when(\"同意\").click()\n    ctx.when(\"确定\").click()\n    # 上面三行代码是立即执行完的，不会有什么等待\n    \n    ctx.wait_stable() # 开启弹窗监控，并等待界面稳定（两个弹窗检查周期内没有弹窗代表稳定）\n\n    # 使用call函数来触发函数回调\n    # call 支持两个参数，d和el，不区分参数位置，可以不传参，如果传参变量名不能写错\n    # eg: 当有元素匹配仲夏之夜，点击返回按钮\n    ctx.when(\"仲夏之夜\").call(lambda d: d.press(\"back\"))\n    ctx.when(\"确定\").call(lambda el: el.click())\n\n    # 其他操作\n\n# 为了方便也可以使用代码中默认的弹窗监控逻辑\n# 下面是目前内置的默认逻辑，可以加群at群主，增加新的逻辑，或者直接提pr\n    # when(\"继续使用\").click()\n    # when(\"移入管控\").when(\"取消\").click()\n    # when(\"^立即(下载|更新)\").when(\"取消\").click()\n    # when(\"同意\").click()\n    # when(\"^(好的|确定)\").click()\nwith d.watch_context(builtin=True) as ctx:\n    # 在已有的基础上增加\n    ctx.when(\"@tb:id/jview_view\").when('//*[@content-desc=\"图片\"]').click()\n\n    # 其他脚本逻辑\n```\n\n另外一种写法\n\n```python\nctx = d.watch_context()\nctx.when(\"设置\").click()\nctx.wait_stable() # 等待界面不在有弹窗了\n\nctx.close()\n```\n\n### 全局设置\n\n```python\nu2.HTTP_TIMEOUT = 60 # 默认值60s, http默认请求超时时间\n```\n\n其他的配置，目前已大部分集中到 `d.settings` 中，根据后期的需求配置可能会有增减。\n\n```python\nprint(d.settings)\n{'operation_delay': (0, 0),\n 'operation_delay_methods': ['click', 'swipe'],\n 'wait_timeout': 20.0}\n\n# 配置点击前延时0.5s，点击后延时1s\nd.settings['operation_delay'] = (.5, 1)\n\n# 修改延迟生效的方法\n# 其中 double_click, long_click 都对应click\nd.settings['operation_delay_methods'] = ['click', 'swipe', 'drag', 'press']\nd.settings['wait_timeout'] = 20.0 # 默认控件等待时间（原生操作，xpath插件的等待时间）\n\nd.settings['max_depth'] = 50 # 默认50，限制dump_hierarchy返回的元素层级\n```\n\n对于随着版本升级，设置过期的配置时，会提示Deprecated，但是不会抛异常。\n\n```bash\n>>> d.settings['click_before_delay'] = 1  \n[W 200514 14:55:59 settings:72] d.settings[click_before_delay] deprecated: Use operation_delay instead\n```\n\nUiAutomator中的超时设置(隐藏方法)\n\n```python\n>> d.jsonrpc.getConfigurator() \n{'actionAcknowledgmentTimeout': 500,\n 'keyInjectionDelay': 0,\n 'scrollAcknowledgmentTimeout': 200,\n 'waitForIdleTimeout': 0,\n 'waitForSelectorTimeout': 0}\n\n>> d.jsonrpc.setConfigurator({\"waitForIdleTimeout\": 100})\n{'actionAcknowledgmentTimeout': 500,\n 'keyInjectionDelay': 0,\n 'scrollAcknowledgmentTimeout': 200,\n 'waitForIdleTimeout': 100,\n 'waitForSelectorTimeout': 0}\n```\n\n为了防止客户端程序响应超时，`waitForIdleTimeout`和`waitForSelectorTimeout`目前已改为`0`\n\nRefs: [Google uiautomator Configurator](https://developer.android.com/reference/android/support/test/uiautomator/Configurator)\n\n## 应用管理\nThis part showcases how to perform app management\n\n### 安装应用\nWe only support installing an APK from a URL\n\n```python\nd.app_install('http://some-domain.com/some.apk')\n```\n\n### 启动应用\n```python\n# 默认的这种方法是先通过atx-agent解析apk包的mainActivity，然后调用am start -n $package/$activity启动\nd.app_start(\"com.example.hello_world\")\n\n# 使用 monkey -p com.example.hello_world -c android.intent.category.LAUNCHER 1 启动\n# 这种方法有个副作用，它自动会将手机的旋转锁定给关掉\nd.app_start(\"com.example.hello_world\", use_monkey=True) # start with package name\n\n# 通过指定main activity的方式启动应用，等价于调用am start -n com.example.hello_world/.MainActivity\nd.app_start(\"com.example.hello_world\", \".MainActivity\")\n```\n\n### 停止应用\n\n```python\n# equivalent to `am force-stop`, thus you could lose data\nd.app_stop(\"com.example.hello_world\") \n# equivalent to `pm clear`\nd.app_clear('com.example.hello_world')\n```\n\n### 停止所有应用\n```python\n# stop all\nd.app_stop_all()\n# stop all app except for com.examples.demo\nd.app_stop_all(excludes=['com.examples.demo'])\n```\n\n### 获取应用信息\n```python\nd.app_info(\"com.examples.demo\")\n# expect output\n#{\n#    \"mainActivity\": \"com.github.uiautomator.MainActivity\",\n#    \"label\": \"ATX\",\n#    \"versionName\": \"1.1.7\",\n#    \"versionCode\": 1001007,\n#    \"size\":1760809\n#}\n\n# save app icon\nimg = d.app_icon(\"com.examples.demo\")\nimg.save(\"icon.png\")\n```\n\n### 列出所有运行的应用\n```python\nd.app_list_running()\n# expect output\n# [\"com.xxxx.xxxx\", \"com.github.uiautomator\", \"xxxx\"]\n```\n\n### 等待应用运行\n```python\npid = d.app_wait(\"com.example.android\") # 等待应用运行, return pid(int)\nif not pid:\n    print(\"com.example.android is not running\")\nelse:\n    print(\"com.example.android pid is %d\" % pid)\n\nd.app_wait(\"com.example.android\", front=True) # 等待应用前台运行\nd.app_wait(\"com.example.android\", timeout=20.0) # 最长等待时间20s（默认）\n```\n\n### 拉取和推送文件\n* push a file to the device\n\n    ```python\n    # push to a folder\n    d.push(\"foo.txt\", \"/sdcard/\")\n    # push and rename\n    d.push(\"foo.txt\", \"/sdcard/bar.txt\")\n    # push fileobj\n    with open(\"foo.txt\", 'rb') as f:\n        d.push(f, \"/sdcard/\")\n    # push and change file access mode\n    d.push(\"foo.sh\", \"/data/local/tmp/\", mode=0o755)\n    ```\n\n* pull a file from the device\n\n    ```python\n    d.pull(\"/sdcard/tmp.txt\", \"tmp.txt\")\n\n    # FileNotFoundError will raise if the file is not found on the device\n    d.pull(\"/sdcard/some-file-not-exists.txt\", \"tmp.txt\")\n    ```\n\n### 其他应用操作\n\n```python\n# grant all the permissions\nd.app_auto_grant_permissions(\"io.appium.android.apis\")\n\n# open scheme\nd.open_url(\"appname://appnamehost\")\n# same as\n# adb shell am start -a android.intent.action.VIEW -d \"appname://appnamehost\"\n```\n\n### Session (Beta)\nSession represent an app lifecycle. Can be used to start app, detect app crash.\n\n* Launch and close app\n\n    ```python\n    sess = d.session(\"com.netease.cloudmusic\") # start 网易云音乐\n    sess.close() # 停止网易云音乐\n    sess.restart() # 冷启动网易云音乐\n    ```\n\n* Use python `with` to launch and close app\n\n    ```python\n    with d.session(\"com.netease.cloudmusic\") as sess:\n        sess(text=\"Play\").click()\n    ```\n\n* Attach to the running app\n\n    ```python\n    # launch app if not running, skip launch if already running\n    sess = d.session(\"com.netease.cloudmusic\", attach=True)\n    ```\n\n* Detect app crash\n\n    ```python\n    # When app is still running\n    sess(text=\"Music\").click() # operation goes normal\n\n    # If app crash or quit\n    sess(text=\"Music\").click() # raise SessionBrokenError\n    # other function calls under session will raise SessionBrokenError too\n    ```\n\n    ```python\n    # check if session is ok.\n    # Warning: function name may change in the future\n    sess.running() # True or False\n    ```\n\n\n## 其他接口\n\n### 停止后台HTTP服务\n通常情况下Python程序退出了，UiAutomation就退出了。\n不过也可以通过接口的方法停止服务\n\n```python\nd.stop_uiautomator()\n```\n\n### 开启调试\n打印出代码背后的HTTP请求信息\n\n```python\n>>> d.debug = True\n>>> d.info\n12:32:47.182 $ curl -X POST -d '{\"jsonrpc\": \"2.0\", \"id\": \"b80d3a488580be1f3e9cb3e926175310\", \"method\": \"deviceInfo\", \"params\": {}}' 'http://127.0.0.1:54179/jsonrpc/0'\n12:32:47.225 Response >>>\n{\"jsonrpc\":\"2.0\",\"id\":\"b80d3a488580be1f3e9cb3e926175310\",\"result\":{\"currentPackageName\":\"com.android.mms\",\"displayHeight\":1920,\"displayRotation\":0,\"displaySizeDpX\":360,\"displaySizeDpY\":640,\"displayWidth\":1080,\"productName\"\n:\"odin\",\"screenOn\":true,\"sdkInt\":25,\"naturalOrientation\":true}}\n<<< END\n```\n\n```python\nfrom uiautomator2 import enable_pretty_logging\nenable_pretty_logging()\n```\n\nOr\n\n```\nlogger = logging.getLogger(\"uiautomator2\")\n# setup logger\n```\n\n## 命令行功能\n其中的`$device_ip`代表设备的ip地址\n\n如需指定设备需要传入`--serial` 如 `python3 -m uiautomator2 --serial bff1234 <SubCommand>`, SubCommand为子命令（screenshot, current 等）\n\n> 1.0.3 Added: `python3 -m uiautomator2` equals to `uiautomator2`\n\n- screenshot: 截图\n\n    ```bash\n    $ uiautomator2 screenshot screenshot.jpg\n    ```\n\n- current: 获取当前包名和activity\n\n    ```bash\n    $ uiautomator2 current\n    {\n        \"package\": \"com.android.browser\",\n        \"activity\": \"com.uc.browser.InnerUCMobile\",\n        \"pid\": 28478\n    }\n    ```\n    \n- uninstall： Uninstall app\n\n    ```bash\n    $ uiautomator2 uninstall <package-name> # 卸载一个包\n    $ uiautomator2 uninstall <package-name-1> <package-name-2> # 卸载多个包\n    $ uiautomator2 uninstall --all # 全部卸载\n    ```\n\n- stop: Stop app\n\n    ```bash\n    $ uiautomator2 stop com.example.app # 停止一个app\n    $ uiautomator2 stop --all # 停止所有的app\n    ```\n\n- doctor:\n\n    ```bash\n    $ uiautomator2 doctor\n    [I 2024-04-25 19:53:36,288 __main__:101 pid:15596] uiautomator2 is OK\n    ```\n\n## Google UiAutomator 2.0和1.x的区别\nhttps://www.cnblogs.com/insist8089/p/6898181.html\n\n- 新增接口：UiObject2、Until、By、BySelector\n- 引入方式：2.0中，com.android.uiautomator.core.* 引入方式被废弃。改为android.support.test.uiautomator\n- 构建系统：Maven 和/或 Ant（1.x）；Gradle（2.0）\n- 产生的测试包的形式：从zip /jar（1.x） 到 apk（2.0）\n- 在本地环境以adb命令运行UIAutomator测试，启动方式的差别：   \n  adb shell uiautomator runtest UiTest.jar -c package.name.ClassName（1.x）\n  adb shell am instrument -e class com.example.app.MyTest \n  com.example.app.test/android.support.test.runner.AndroidJUnitRunner（2.0）\n- 能否使用Android服务及接口？ 1.x~不能；2.0~能。\n- og输出？ 使用System.out.print输出流回显至执行端（1.x）； 输出至Logcat（2.0）\n- 执行？测试用例无需继承于任何父类，方法名不限，使用注解 Annotation进行（2.0）;  需要继承UiAutomatorTestCase，测试方法需要以test开头(1.x) \n\n\n## 依赖项目\n- [![PyPI](https://img.shields.io/pypi/v/adbutils.svg?label=adbutils)](https://github.com/openatx/adbutils)\n- [![GitHub tag (latest SemVer)](https://img.shields.io/github/tag/openatx/android-uiautomator-server.svg?label=android-uiautomator-server)](https://github.com/openatx/android-uiautomator-server) 已迁移到私有仓库，需要合作开发进QQ群联系群主\n\n# Contributors\n\n[contributors](../../graphs/contributors)\n\n# 其他优秀的项目\n\n- https://github.com/ecnusse/Kea2: 面向安卓的自动化界面遍历与脚本协同测试框架\n- https://github.com/atinfo/awesome-test-automation 所有优秀测试框架的集合，包罗万象\n- [google/mobly](https://github.com/google/mobly) 谷歌内部的测试框架，虽然我不太懂，但是感觉很好用\n- https://github.com/zhangzhao4444/Maxim 基于Uiautomator的monkey\n- http://www.sikulix.com/ 基于图像识别的自动化测试框架，非常的老牌\n- http://airtest.netease.com/ 本项目的前身，后来被网易广州团队接手并继续优化。实现有一个不错的IDE (archived)\n\n排名有先后，欢迎补充\n\n# LICENSE\n[MIT](LICENSE)"
  },
  {
    "path": "XPATH.md",
    "content": "# uiautomator2 XPath Extension\n\n[📖 阅读中文版](XPATH_CN.md)\n\nBefore using this plugin, you need to understand some XPath knowledge. Fortunately, there are many convenient resources available online. Below are some examples:\n\n- [W3CSchool XPath Tutorial](http://www.w3school.com.cn/xpath/index.asp)\n- [XPath Tutorial](http://www.zvon.org/xxl/XPathTutorial/)\n- [Ruan Yifeng’s XPath Learning Notes](http://www.ruanyifeng.com/blog/2009/07/xpath_path_expressions.html)\n- [Website for Testing XPath](https://www.freeformatter.com/xpath-tester.html)\n- [XPath Tester](https://extendsclass.com/xpath-tester.html)\n\nThe code has not been fully tested and may still have bugs. Feedback is welcome.\n\n## How It Works\n\n1. Use the `dump_hierarchy` interface from the `uiautomator2` library to obtain the current UI screen (a comprehensive XML).\n2. Then use the `lxml` library to parse and search for matching XPath expressions, and perform click operations using the `click` command.\n\n> Currently, `lxml` only supports XPath 1.0. If anyone knows how to support XPath 2.0, please let me know.\n\n**Popup Monitoring Principle**\n\nThe hierarchy provides information about all elements on the screen (including popups and buttons to be clicked). Suppose there are two popup buttons: `Skip` and `Got It`. The button to be clicked is `Play`.\n\n1. Obtain the current screen’s XML (using the `dump_hierarchy` function).\n2. Check if the `Skip` or `Got It` buttons are present. If they are, click them and return to step 1.\n3. Check if the `Play` button is present. If it is, click it and finish. If not found, return to step 1 and keep executing until the search attempts exceed the limit.\n\n## Installation\n\n```bash\npip3 install -U uiautomator2\n```\n\n## Usage\n\n### Simple Usage\n\nCheck out the following simple example to understand how to use it:\n\n```python\nimport uiautomator2 as u2\n\ndef main():\n    d = u2.connect()\n    d.app_start(\"com.netease.cloudmusic\", stop=True)\n\n    d.xpath('//*[@text=\"Private FM\"]').click()\n    \n    #\n    # Advanced Usage (Element Positioning)\n    #\n\n    # Starting with @\n    d.xpath('@personal-fm') # Equivalent to d.xpath('//*[@resource-id=\"personal-fm\"]')\n    \n    # Multiple condition positioning, similar to AND\n    d.xpath('//android.widget.Button').xpath('//*[@text=\"Private FM\"]')\n    \n    d.xpath('//*[@text=\"Private FM\"]').parent() # Position to the parent element\n    d.xpath('//*[@text=\"Private FM\"]').parent(\"@android:list\") # Position to the parent element that meets the condition\n\n    # When using child, it is not recommended to use multiple condition XPath because it can be confusing\n    d.xpath('@android:id/list').child('/android.widget.TextView').click()\n    # Equivalent to the following\n    # d.xpath('//*[@resource-id=\"android:id/list\"]/android.widget.TextView').click()\n```\n\n> For convenience, the following code does not include `import` and `main`. It is assumed that the variable `d` exists.\n\n### Operations of `XPathSelector`\n\n```python\nsl = d.xpath(\"@com.example:id/home_searchedit\") # sl is an XPathSelector object\n\n# Click\nsl.click()\nsl.click(timeout=10) # Specify a timeout, throws XPathElementNotFoundError if not found\nsl.click_exists() # Click if exists, returns whether the click was successful\nsl.click_exists(timeout=10) # Wait up to 10 seconds\n\nsl.match() # Returns None if not matched, otherwise returns an XMLElement\n\n# Wait for the corresponding element to appear, returns XMLElement\n# The default waiting time is 10 seconds\nel = sl.wait()\nel = sl.wait(timeout=15) # Wait for 15 seconds, returns None if not found\n\n# Wait for the element to disappear\nsl.wait_gone()\nsl.wait_gone(timeout=15) \n\n# Similar to wait, but throws XPathElementNotFoundError if not found\nel = sl.get() \nel = sl.get(timeout=15)\n\n# Change the default waiting time to 15 seconds\nd.xpath.global_set(\"timeout\", 15)\nd.xpath.implicitly_wait(15) # Equivalent to the previous line (TODO: Removed)\n\nprint(sl.exists) # Returns whether it exists (bool)\nsl.get_last_match() # Get the last matched XMLElement\n\nsl.get_text() # Get the component name\nsl.set_text(\"\") # Clear the input box\nsl.set_text(\"hello world\") # Input \"hello world\" into the input box\n\n# Iterate through all matched elements\nfor el in d.xpath('//android.widget.EditText').all():\n    print(\"rect:\", el.rect) # Output tuple: (x, y, width, height)\n    print(\"center:\", el.center())\n    el.click() # Click operation\n    print(el.elem) # Output the Node parsed by lxml\n    print(el.text)\n\n# Child operation\nd.xpath('@android:id/list').child('/android.widget.TextView').click()\n# Equivalent to d.xpath('//*[@resource-id=\"android:id/list\"]/android.widget.TextView').all()\n```\n\n### Advanced Search Syntax\n\n> Added in version 3.1\n\n```python\n# Find text=NFC AND id=android:id/item\n(d.xpath(\"NFC\") & d.xpath(\"@android:id/item\")).get()\n\n# Find text=NFC OR id=android:id/item\n(d.xpath(\"NFC\") | d.xpath(\"App\") | d.xpath(\"Content\")).get()\n\n# Supports more complex queries\n((d.xpath(\"NFC\") | d.xpath(\"@android:id/item\")) & d.xpath(\"//android.widget.TextView\")).get()\n```\n\n### Operations of `XMLElement`\n\n```python\n# The object returned by XPathSelector.get() is called XMLElement\nel = d.xpath(\"@com.example:id/home_searchedit\").get()\n\nlx, ly, width, height = el.rect # Get the top-left coordinates and size\nlx, ly, rx, ry = el.bounds # Top-left and bottom-right coordinates\nx, y = el.center() # Get the element’s center position\nx, y = el.offset(0.5, 0.5) # Same as center()\n\n# Send click\nel.click()\n\n# Print text content\nprint(el.text) \n\n# Get the attributes within the group, as a dict\nprint(el.attrib)\n\n# Take a screenshot of the control (the principle is to take a full screenshot first, then crop)\nel.screenshot()\n\n# Swipe the control\nel.swipe(\"right\") # left, right, up, down\nel.swipe(\"right\", scale=0.9) # scale defaults to 0.9, meaning the swipe distance is 90% of the control's width. Swiping up uses 90% of the height.\n\nprint(el.info)\n# Output example\n{\n 'index': '0',\n 'text': '',\n 'resourceId': 'com.example:id/home_searchedit',\n 'checkable': 'true',\n 'checked': 'true',\n 'clickable': 'true',\n 'enabled': 'true',\n 'focusable': 'false',\n 'focused': 'false',\n 'scrollable': 'false',\n 'longClickable': 'false',\n 'password': 'false',\n 'selected': 'false',\n 'visibleToUser': 'true',\n 'childCount': 0,\n 'className': 'android.widget.Switch',\n 'bounds': {'left': 882, 'top': 279, 'right': 1026, 'bottom': 423},\n 'packageName': 'com.android.settings',\n 'contentDescription': '',\n 'resourceName': 'android:id/switch_widget'\n}\n```\n\n### Swipe to a Specified Position\n\n> The `scroll_to` feature is newly added and may not be fully polished (for example, it cannot detect if it has scrolled to the bottom).\n\nFirst, see the example:\n\n```python\nfrom uiautomator2 import connect_usb, Direction\n\nd = connect_usb()\n\nd.scroll_to(\"Place Order\")\nd.scroll_to(\"Place Order\", Direction.FORWARD) # Defaults to scrolling down. Other options include BACKWARD, HORIZ_FORWARD (horizontal), HORIZ_BACKWARD (horizontal reverse)\nd.scroll_to(\"Place Order\", Direction.HORIZ_FORWARD, max_swipes=5)\n\n# Additionally, you can scroll within a specified element\nd.xpath('@com.taobao.taobao:id/dx_root').scroll(Direction.HORIZ_FORWARD)\nd.xpath('@com.taobao.taobao:id/dx_root').scroll_to(\"Place Order\", Direction.HORIZ_FORWARD)\n```\n\n**A More Complete Example**\n\n```python\nimport uiautomator2 as u2\nfrom uiautomator2 import Direction\n\ndef main():\n    d = u2.connect()\n    d.app_start(\"com.netease.cloudmusic\", stop=True)\n\n    # Steps\n    d.xpath(\"//*[@text='Private FM']/../android.widget.ImageView\").click()\n    d.xpath(\"Next Song\").click()\n\n    # Monitor popups for 2 seconds, the time may exceed 2 seconds\n    d.xpath.sleep_watch(2)\n    d.xpath(\"Go to Previous Level\").click()\n    \n    d.xpath(\"Go to Previous Level\").click(watch=False) # Click without triggering watch\n    d.xpath(\"Go to Previous Level\").click(timeout=5.0) # Wait timeout 5 seconds\n\n    d.xpath.watch_background() # Enable background monitoring mode, checks every 4 seconds by default\n    d.xpath.watch_background(interval=2.0) # Check every 2 seconds\n    d.xpath.watch_stop() # Stop monitoring\n\n    for el in d.xpath('//android.widget.EditText').all():\n        print(\"rect:\", el.rect) # Output tuple: (left_x, top_y, width, height)\n        print(\"bounds:\", el.bounds) # Output tuple: (left, top, right, bottom)\n        print(\"center:\", el.center())\n        el.click() # Click operation\n        print(el.elem) # Output the Node parsed by lxml\n\n    # Swiping\n    el = d.xpath('@com.taobao.taobao:id/fl_banner_container').get()\n\n    # Swipe from right to left\n    el.swipe(Direction.HORIZ_FORWARD) \n    el.swipe(Direction.LEFT) # Swipe from right to left\n\n    # Swipe from bottom to top\n    el.swipe(Direction.FORWARD)\n    el.swipe(Direction.UP)\n\n    el.swipe(\"right\", scale=0.9) # scale defaults to 0.9, swipe distance is 80% of the control's width, the swipe center aligns with the control's center\n    el.swipe(\"up\", scale=0.5) # Swipe distance is 50% of the control's height\n\n    # scroll is different from swipe; scroll returns a bool indicating whether new elements appeared\n    el.scroll(Direction.FORWARD) # Swipe down\n    el.scroll(Direction.BACKWARD) # Swipe up\n    el.scroll(Direction.HORIZ_FORWARD) # Swipe horizontally forward\n    el.scroll(Direction.HORIZ_BACKWARD) # Swipe horizontally backward\n\n    if el.scroll(\"forward\"):\n        print(\"Can continue scrolling\")\n```\n\n### `PageSource` Object\n\n> Added in version 3.1\n\nThis is an advanced usage, but this object is also the most fundamental, as almost all functions depend on it.\n\n**What is PageSource?**\n\nPageSource is initialized from the return value of `d.dump_hierarchy()`. It is mainly used to find elements through XPath.\n\n**Usage:**\n\n```python\nsource = d.xpath.get_page_source()\n\n# find_elements is the core method\nelements = source.find_elements('//android.widget.TextView') # List[XMLElement]\nfor el in elements:\n    print(el.text)\n\n# Get coordinates and click\nx, y = elements[0].center()\nd.click(x, y)\n\n# Multiple condition query syntax\nes1 = source.find_elements('//android.widget.TextView')\nes2 = source.find_elements(XPath('@android:id/content').joinpath(\"//*\"))\n\n# Find TextViews that do not belong to nodes under id=android:id/content\nels = set(es1) - set(es2)\n\n# Find TextViews that belong to nodes under id=android:id/content\nels = set(es1) & set(es2)\n```\n\n## XPath Rules\n\nTo write scripts faster, we have customized some simplified XPath rules.\n\n**Rule 1**\n\nStarting with `//` represents native XPath.\n\n**Rule 2**\n\nStarting with `@` represents resourceId positioning.\n\n`@smartisanos:id/right_container` is equivalent to `//*[@resource-id=\"smartisanos:id/right_container\"]`\n\n**Rule 3**\n\nStarting with `^` represents a regular expression.\n\n`^.*done` is equivalent to `//*[re:match(text(), '^.*done')]`\n\n**Rule 4**\n\n> Inspired by SQL LIKE\n\n`Know%` matches text starting with `Know`, equivalent to `//*[starts-with(text(), 'Know')]`\n\n`%Know` matches text ending with `Know`, equivalent to `//*[ends-with(text(), 'Know')]`\n\n`%Know%` matches text containing `Know`, equivalent to `//*[contains(text(), 'Know')]`\n\n**Last Rule**\n\nMatches both `text` and `description` fields.\n\nFor example, `Search` is equivalent to XPath `//*[@text=\"Search\" or @content-desc=\"Search\" or @resource-id=\"Search\"]`\n\n## Special Notes\n\n- Sometimes, `className` contains characters like `$@#&`, which are invalid in XML. Therefore, they are all replaced with `.`.\n\n## Some Advanced Uses of XPath\n\n```\n# All elements\n//*\n\n# Elements where resource-id contains 'login'\n//*[contains(@resource-id, 'login')]\n\n# Buttons containing 'Account' or 'Account Number'\n/android.widget.Button[contains(@text, 'Account') or contains(@text, 'Account Number')]\n\n# The second element among all ImageViews\n(//android.widget.ImageView)[2]\n\n# The last element among all ImageViews\n(//android.widget.ImageView)[last()]\n\n# Elements where className contains 'ImageView'\n//*[contains(name(), \"ImageView\")]\n```\n\n## Some Useful Websites\n\n- [XPath Playground](https://scrapinghub.github.io/xpath-playground/)\n- [Some Advanced Uses of XPath - JianShu](https://www.jianshu.com/p/4fef4142b33f)\n- [XPath Quicksheet](https://devhints.io/xpath)\n\nIf you have other resources, feel free to submit [Issues](https://github.com/openatx/uiautomator2/issues/new) to contribute."
  },
  {
    "path": "XPATH_CN.md",
    "content": "# uiautomator2 xpath extension\n\n[📖 Read the English version](XPATH.md)\n\n用这个插件前，要先了解一些XPath知识。\n好在网上这方便的资料很多。下面列举一些\n\n- [W3CSchool XPath教程](http://www.w3school.com.cn/xpath/index.asp)\n- [XPath tutorial](http://www.zvon.org/xxl/XPathTutorial/)\n- [阮一峰的XPath学习笔记](http://www.ruanyifeng.com/blog/2009/07/xpath_path_expressions.html)\n- [测试XPath的网站](https://www.freeformatter.com/xpath-tester.html)\n- [XPath tester](https://extendsclass.com/xpath-tester.html)\n\n代码并没有完全测试完，可能还有bug，欢迎跟我反馈。\n\n## 工作原理\n1. 通过uiautomator2库的`dump_hierarchy`接口，获取到当前的UI界面（一个很丰富的XML）。\n2. 然后使用`lxml`库解析，寻找匹配的xpath，然后使用click指令完后操作\n\n>目前发现lxml只支持XPath1.0, 有了解的可以告诉我下怎么支持XPath2.0\n\n**弹窗监控原理**\n\n通过hierarchy可以知道界面上的所有元素信息（包括弹窗和要点击的按钮）。\n假设有 `跳过`, `知道了` 这两个弹窗按钮。需要点击的按钮名是 `播放`\n\n1. 获取到当前界面的XML（通过dump_hierarchy函数）\n2. 检查有没有`跳过`, `知道了` 这两个按钮，如果有就点击，然后回到第一步\n3. 检查有没有`播放`按钮, 有就点击，结束。没有找到在回到第一步，一直执行到查找次数超标。\n\n## 安装方法\n```\npip3 install -U uiautomator2\n```\n\n## 使用方法\n\n### 简单用法\n\n看下面的这个简单的例子了解下如何使用\n\n```python\nimport uiautomator2 as u2\n\ndef main():\n    d = u2.connect()\n    d.app_start(\"com.netease.cloudmusic\", stop=True)\n\n    d.xpath('//*[@text=\"私人FM\"]').click()\n    \n    #\n    # 高级用法(元素定位)\n    #\n\n    # @开头\n    d.xpath('@personal-fm') # 等价于 d.xpath('//*[@resource-id=\"personal-fm\"]')\n    # 多个条件定位, 类似于AND\n    d.xpath('//android.widget.Button').xpath('//*[@text=\"私人FM\"]')\n    \n    d.xpath('//*[@text=\"私人FM\"]').parent() # 定位到父元素\n    d.xpath('//*[@text=\"私人FM\"]').parent(\"@android:list\") # 定位到符合条件的父元素\n\n\t# 包含child的时候，不建议在使用多条件的xpath，因为容易搞混\n\td.xpath('@android:id/list').child('/android.widget.TextView').click()\n\t# 等价于下面这个\n\t# d.xpath('//*[@resource-id=\"android:id/list\"]/android.widget.TextView').click()\n```\n\n>下面的代码为了方便就不写`import`和`main`了，默认存在`d`这个变量\n\n### `XPathSelector`的操作\n\n```python\nsl = d.xpath(\"@com.example:id/home_searchedit\") # sl为XPathSelector对象\n\n# 点击\nsl.click()\nsl.click(timeout=10) # 指定超时时间, 找不到抛出异常 XPathElementNotFoundError\nsl.click_exists() # 存在即点击，返回是否点击成功\nsl.click_exists(timeout=10) # 等待最多10s钟\n\nsl.match() # 不匹配返回None, 否则返回XMLElement\n\n# 等到对应的元素出现，返回XMLElement\n# 默认的等待时间是10s\nel = sl.wait()\nel = sl.wait(timeout=15) # 等待15s, 没有找到会返回None\n\n# 等待元素消失\nsl.wait_gone()\nsl.wait_gone(timeout=15) \n\n# 跟wait用法类似，区别是如果没找到直接抛出 XPathElementNotFoundError 异常\nel = sl.get() \nel = sl.get(timeout=15)\n\n# 修改默认的等待时间为15s\nd.xpath.global_set(\"timeout\", 15)\nd.xpath.implicitly_wait(15) # 与上一行代码等价 (TODO: Removed)\n\nprint(sl.exists) # 返回是否存在 (bool)\nsl.get_last_match() # 获取上次匹配的XMLElement\n\nsl.get_text() # 获取组件名\nsl.set_text(\"\") # 清空输入框\nsl.set_text(\"hello world\") # 输入框输入 hello world\n\n# 遍历所有匹配的元素\nfor el in d.xpath('//android.widget.EditText').all():\n    print(\"rect:\", el.rect) # output tuple: (x, y, width, height)\n    print(\"center:\", el.center())\n    el.click() # click operation\n    print(el.elem) # 输出lxml解析出来的Node\n    print(el.text)\n\n# child操作\nd.xpath('@android:id/list').child('/android.widget.TextView').click()\n等价于 d.xpath('//*[@resource-id=\"android:id/list\"]/android.widget.TextView').all()\n```\n\n高级查找语法\n\n> Added in version 3.1\n\n```python\n# 查找 text=NFC AND id=android:id/item\n(d.xpath(\"NFC\") & d.xpath(\"@android:id/item\")).get()\n\n# 查找 text=NFC OR id=android:id/item\n(d.xpath(\"NFC\") | d.xpath(\"App\") | d.xpath(\"Content\")).get()\n\n# 复杂一点也支持\n((d.xpath(\"NFC\") | d.xpath(\"@android:id/item\")) & d.xpath(\"//android.widget.TextView\")).get()\n\n### `XMLElement`的操作\n\n```python\n# 通过XPathSelector.get() 返回的对象叫做 XMLElement\nel = d.xpath(\"@com.example:id/home_searchedit\").get()\n\nlx, ly, width, height = el.rect # 获取左上角坐标和宽高\nlx, ly, rx, ry = el.bounds # 左上角与右下角的坐标\nx, y = el.center() # get element center position\nx, y = el.offset(0.5, 0.5) # same as center()\n\n# send click\nel.click()\n\n# 打印文本内容\nprint(el.text) \n\n# 获取组内的属性, dict类型\nprint(el.attrib)\n\n# 控件截图 （原理为先整张截图，然后再crop）\nel.screenshot()\n\n# 控件滑动\nel.swipe(\"right\") # left, right, up, down\nel.swipe(\"right\", scale=0.9) # scale默认0.9, 意思是滑动距离为控件宽度的90%, 上滑则为高度的90%\n\nprint(el.info)\n# output example\n{'index': '0',\n 'text': '',\n 'resourceId': 'com.example:id/home_searchedit',\n 'checkable': 'true',\n 'checked': 'true',\n 'clickable': 'true',\n 'enabled': 'true',\n 'focusable': 'false',\n 'focused': 'false',\n 'scrollable': 'false',\n 'longClickable': 'false',\n 'password': 'false',\n 'selected': 'false',\n 'visibleToUser': 'true',\n 'childCount': 0,\n 'className': 'android.widget.Switch',\n 'bounds': {'left': 882, 'top': 279, 'right': 1026, 'bottom': 423},\n 'packageName': 'com.android.settings',\n 'contentDescription': '',\n 'resourceName': 'android:id/switch_widget'}\n```\n\n### 滑动到指定位置\n> `scroll_to` 这个功能属于新增加的，可能不这么完善（比如不能检测是否滑动到底部了）\n\n先看例子\n\n```python\nfrom uiautomator2 import connect_usb, Direction\n\nd = connect_usb()\n\nd.scroll_to(\"下单\")\nd.scroll_to(\"下单\", Direction.FORWARD) # 默认就是向下滑动，除此之外还可以BACKWARD, HORIZ_FORWARD(水平), HORIZ_BACKWARD(水平反向)\nd.scroll_to(\"下单\", Direction.HORIZ_FORWARD, max_swipes=5)\n\n# 除此之外还可以在指定在某个元素内滑动\nd.xpath('@com.taobao.taobao:id/dx_root').scroll(Direction.HORIZ_FORWARD)\nd.xpath('@com.taobao.taobao:id/dx_root').scroll_to(\"下单\", Direction.HORIZ_FORWARD)\n```\n\n**比较完整的例子**\n\n```python\nimport uiautomator2 as u2\nfrom uiautomator2 import Direction\n\ndef main():\n    d = u2.connect()\n    d.app_start(\"com.netease.cloudmusic\", stop=True)\n\n    # steps\n    d.xpath(\"//*[@text='私人FM']/../android.widget.ImageView\").click()\n    d.xpath(\"下一首\").click()\n\n    # 监控弹窗2s钟，时间可能大于2s\n    d.xpath.sleep_watch(2)\n    d.xpath(\"转到上一层级\").click()\n    \n    d.xpath(\"转到上一层级\").click(watch=False) # click without trigger watch\n    d.xpath(\"转到上一层级\").click(timeout=5.0) # wait timeout 5s\n\n    d.xpath.watch_background() # 开启后台监控模式，默认每4s检查一次\n    d.xpath.watch_background(interval=2.0) # 每2s检查一次\n    d.xpath.watch_stop() # 停止监控\n\n    for el in d.xpath('//android.widget.EditText').all():\n        print(\"rect:\", el.rect) # output tuple: (left_x, top_y, width, height)\n        print(\"bounds:\", el.bounds) # output tuple: （left, top, right, bottom)\n        print(\"center:\", el.center())\n        el.click() # click operation\n        print(el.elem) # 输出lxml解析出来的Node\n    \n    # 滑动\n    el = d.xpath('@com.taobao.taobao:id/fl_banner_container').get()\n\n    # 从右滑到左\n    el.swipe(Direction.HORIZ_FORWARD) \n    el.swipe(Direction.LEFT) # 从右滑到左\n\n    # 从下滑到上\n    el.swipe(Direction.FORWARD)\n    el.swipe(Direction.UP)\n\n    el.swipe(\"right\", scale=0.9) # scale 默认0.9, 滑动距离为控件宽度的80%,\b 滑动的中心点与控件中心点一致\n    el.swipe(\"up\", scale=0.5) # 滑动距离为控件高度的50%\n\n    # scroll同swipe不一样，scroll返回bool值，表示是否还有新元素出现\n    el.scroll(Direction.FORWARD) # 向下滑动\n    el.scroll(Direction.BACKWARD) # 向上滑动\n    el.scroll(Direction.HORIZ_FORWARD) # 水平向前\n    el.scroll(Direction.HORIZ_BACKWARD) # 水平向后\n\n    if el.scroll(\"forward\"):\n        print(\"还可以继续滚动\")\n```\n\n### `PageSource`对象\n> Added in version 3.1\n\n这个属于高级用法，但是这个对象也最初级，几乎所有的函数都依赖它。\n\n什么是PageSource？\n\nPageSource是从d.dump_hierarchy()的返回值初始化来的。主要用于通过XPATH完成元素的查找工作。\n\n用法？\n\n```python\nsource = d.xpath.get_page_source()\n\n# find_elements 是核心方法\nelements = source.find_elements('//android.widget.TextView') # List[XMLElement]\nfor el in elements:\n    print(el.text)\n\n# 获取坐标后点击\nx, y = elements[0].center()\nd.click(x, y)\n\n# 多种条件的查询写法\nes1 = source.find_elements('//android.widget.TextView')\nes2 = source.find_elements(XPath('@android:id/content').joinpath(\"//*\"))\n\n# 寻找是TextView但不属于id=android:id/content下的节点\nels = set(es1) - set(es2)\n\n# 寻找是TextView同事属于id=android:id/content下的节点\nels = set(es1) & set(es2)\n```\n\n## XPath规则\n为了写起脚本来更快，我们自定义了一些简化的xpath规则\n\n**规则1**\n\n`//` 开头代表原生xpath\n\n**规则2**\n\n`@` 开头代表resourceId定位\n\n`@smartisanos:id/right_container` 相当于 \n`//*[@resource-id=\"smartisanos:id/right_container\"]`\n\n**规则3**\n\n`^`开头代表正则表达式\n\n`^.*道了` 相当于 `//*[re:match(text(), '^.*道了')]`\n\n**规则4**\n\n> 灵感来自SQL like\n\n`知道%` 匹配`知道`开始的文本， 相当于 `//*[starts-with(text(), '知道')]`\n\n`%知道` 匹配`知道`结束的文本，相当于 `//*[ends-with(text(), '知道')]`\n\n`%知道%` 匹配包含`知道`的文本，相当于 `//*[contains(text(), '知道')]`\n\n**规则 Last**\n\n会匹配text 和 description字段\n\n如 `搜索` 相当于 XPath `//*[@text=\"搜索\" or @content-desc=\"搜索\" or @resource-id=\"搜索\"]`\n\n## 特殊说明\n- 有时className中包含有`$@#&`字符，这个字符在XML中是不合法的，所以全部替换成了`.`\n\n## XPath的一些高级用法\n```\n# 所有元素\n//*\n\n# resource-id包含login字符\n//*[contains(@resource-id, 'login')]\n\n# 按钮包含账号或帐号\n//android.widget.Button[contains(@text, '账号') or contains(@text, '帐号')]\n\n# 所有ImageView中的第二个\n(//android.widget.ImageView)[2]\n\n# 所有ImageView中的最后一个\n(//android.widget.ImageView)[last()]\n\n# className包含ImageView\n//*[contains(name(), \"ImageView\")]\n```\n\n## 一些有用的网站\n- [XPath playground](https://scrapinghub.github.io/xpath-playground/)\n- [XPath的一些高级用法-简书](https://www.jianshu.com/p/4fef4142b33f)\n- [XPath Quicksheet](https://devhints.io/xpath)\n\n如有其他资料，欢迎提[Issues](https://github.com/openatx/uiautomator2/issues/new)补充\n"
  },
  {
    "path": "_archived/aircv/README.md",
    "content": "# 前言\n\n这是一个 uiautimator2 的一个插件，使得 uiautimator2 可以支持通过图像识别来对手机进行操作  \n代码集成了开源库: [aircv](https://github.com/NetEaseGame/aircv)\n\n\n# 注意\n\n1. 只能支持常宽比为 16:9 的手机\n2. 截图是以 atx-agent 传过来的图像为基准，图片大小为 800*450  \n    因为分辨率小了，会有失真，所以匹配阈值可适当减小（下面有说明）\n3. 因为基准图为 800*450 分辨率，area 区域范围不能大于该分辨率（下面有说明）\n4. 有时候确实有但是查找不到，或者查找错误，可适当截图截的大一点\n\n\n# 环境\nopencv3.x\n1. 安装opencv3 支持py2和py3（测试环境 python2.7.15 和 python3.7.0）\n    ```bash\n    pip install opencv_python\n    ```\n2. 安装 websocket\n    ```bash\n    pip install websocket\n    ```\n3. 安装 numpy\n    ```bash\n    pip install numpy\n    ```\n\n\n# 设置\n\n```python\n# 启用支持网络下载图片选项\nAircv.support_network = True  # 默认 False，不启用\n# 设置 host，支持 http\nAircv.host = \"127.0.0.1:8000\"\n# 请求路径，固定\nAircv.path = \"/image_service/download/\"\n# 示例，图片请求地址\nimg_url = \"http://127.0.0.1:8000/image_service/download/@img1\"\n\n\n# 全局设置操作的超时时间，大于该值时间没有找到图像，会报异常\n# timeout 可以在每个函数调用时单独设置\nAircv.timeout = 30\n\n# 全局设置操作的等待时间，该值为在查找到图像后，等待多久再操作（等待UI元素渲染完成）\n# 例如点击操作，查找到图像后，等待 1秒，然后才点击\nAircv.wait_before_operation = 1\n\n# 全局设置读取图像的频率，间隔几秒读取一张图像，默认为 2秒\n# 手机端的服务会 atx-agent 会以较高频率不断发送 800*450 的图像过来，设置该值限制频率\nAircv.rcv_interval = 2\n\n# 图像查找采用模板匹配的方式\n# 该设置定义阈值，大于该阈值，则认为图像相同，即找到图像\n# 一般来说大于0.999认为图像一样，阈值默认值为0.95\nCVHandler.template_threshold = 0.95\n\n```\n\n\n# 图像传输\n\n> 一般来说，图像传输会在连接上设备开始传输，程序结束会自动关闭传输\n> 如果需要主动关闭，开启图像传输的话，可参考如下\n```python\nimport uiautomator2 as u2\nfrom aircv import Aircv\n\nu2.plugin_register('aircv', Aircv)\nd = u2.connect()\n\n# 关闭图像传输\nd.ext_aircv.stop_get_scren()\n\n# 开启图像传输\nd.ext_aircv.start_get_screen()\n\n```\n\n\n# 示例\n\n```python\nimport uiautomator2 as u2\nfrom aircv import Aircv\n\nu2.plugin_register('aircv', Aircv)\nd = u2.connect()\n\n\n# 判断是否存在\nd.ext_aircv.exists('tmp.jpg')\nd.ext_aircv.exists('tmp.jpg', timeout=60)  # 设置超时时间\n\n\n# 点击\nd.ext_aircv.click('tmp.jpg')\nd.ext_aircv.click('tmp.jpg', timeout=60)  # 设置超时时间\n\n\n# 原图像中指定查找范围，安卓以左上角为原点即（0,0）\n# 参数传入左上角坐标和右下角坐标(x1, y1, x2, y2)\nd.ext_aircv.click('tmp.jpg', area=(100, 100, 300, 200))\n\n\n# 长按\nd.ext_aircv.long_click('tmp.jpg')\nd.ext_aircv.long_click('tmp.jpg', duration=5)  # 设置长按时间\nd.ext_aircv.long_click('tmp.jpg', timeout=60)  # 设置超时时间\nd.ext_aircv.long_click('tmp.jpg', area=(100, 100, 300, 200))  # 设置查找范围\n\n\n# 滑动\nd.ext_aircv.swipe('tmp1.jpg', 'tmp2.jpg')\n\n#设置持续时间，0.1 表示持续 1秒， 默认 1秒\nd.ext_aircv.swipe('tmp1.jpg', 'tmp2.jpg', duration=0.1)\nd.ext_aircv.swipe('tmp1.jpg', 'tmp2.jpg', timeout=60)  # 设置超时时间\nd.ext_aircv.swipe('tmp1.jpg', 'tmp2.jpg', area=(100, 100, 300, 200))  # 设置查找范围\n\n\n# 多点滑动\n# duration 的值， 0.1 表示持续 1秒， 默认 1秒\nimg_list = ['tmp1.jpg', 'tmp2.jpg', 'tmp3.jpg']\nd.ext_aircv.swipe_points(img_list, duration=0.5, timeout=60)\n\n\n# 拖动（按住一会再滑动）\nd.ext_aircv.drag('tmp1.jpg', 'tmp2.jpg', duration=0.1, timeout=60)\nd.ext_aircv.drag('tmp1.jpg', 'tmp2.jpg', area=(100, 100, 300, 200))  # 设置查找范围\n\n\n# 获取坐标(x, y)（返回查找到图像的中心坐标）\nd.ext_aircv.get_point('tmp1.jpg', timeout=60, area=(100, 100, 300, 200)) \n\n```\n"
  },
  {
    "path": "_archived/aircv/__init__.py",
    "content": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n\nimport threading\nimport time\n\nimport cv2\nimport numpy as np\nimport requests\nimport websocket\n\n__version__ = \"0.0.1\"\n\n\nclass CVHandler(object):\n    template_threshold = 0.95  # 模板匹配的阈值\n\n    def show(self, img):\n        ''' 显示一个图片 '''\n        cv2.imshow('image', img)\n        cv2.waitKey(0)\n        cv2.destroyAllWindows()\n\n    def imread(self, filename):\n        '''\n        Like cv2.imread\n        This function will make sure filename exists\n        '''\n        im = cv2.imread(filename)\n        if im is None:\n            raise RuntimeError(\"file: '%s' not exists\" % filename)\n        return im\n\n    def imdecode(self, img_data):\n        '''\n        Like cv2.imdecode\n        This function will make sure filename exists\n        直接读取从网络下载的图片数据\n        '''\n        im = np.asarray(bytearray(img_data), dtype=\"uint8\")\n        im = cv2.imdecode(im, cv2.IMREAD_COLOR)\n        if im is None:\n            raise RuntimeError(\"img_data is can not decode\")\n        return im\n\n    def find_template(self, im_source, im_search, threshold=template_threshold, rgb=False, bgremove=False):\n        '''\n        @return find location\n        if not found; return None\n        '''\n        result = self.find_all_template(im_source, im_search, threshold, 1, rgb, bgremove)\n        return result[0] if result else None\n\n    def find_all_template(self, im_source, im_search, threshold=template_threshold, maxcnt=0, rgb=False,\n                          bgremove=False):\n        '''\n        Locate image position with cv2.templateFind\n\n        Use pixel match to find pictures.\n\n        Args:\n            im_source(string): 图像、素材\n            im_search(string): 需要查找的图片\n            threshold: 阈值，当相识度小于该阈值的时候，就忽略掉\n\n        Returns:\n            A tuple of found [(point, score), ...]\n\n        Raises:\n            IOError: when file read error\n        '''\n        # method = cv2.TM_CCORR_NORMED\n        # method = cv2.TM_SQDIFF_NORMED\n        method = cv2.TM_CCOEFF_NORMED\n\n        if rgb:\n            s_bgr = cv2.split(im_search)  # Blue Green Red\n            i_bgr = cv2.split(im_source)\n            weight = (0.3, 0.3, 0.4)\n            resbgr = [0, 0, 0]\n            for i in range(3):  # bgr\n                resbgr[i] = cv2.matchTemplate(i_bgr[i], s_bgr[i], method)\n            res = resbgr[0] * weight[0] + resbgr[1] * weight[1] + resbgr[2] * weight[2]\n        else:\n            s_gray = cv2.cvtColor(im_search, cv2.COLOR_BGR2GRAY)\n            i_gray = cv2.cvtColor(im_source, cv2.COLOR_BGR2GRAY)\n            # 边界提取(来实现背景去除的功能)\n            if bgremove:\n                s_gray = cv2.Canny(s_gray, 100, 200)\n                i_gray = cv2.Canny(i_gray, 100, 200)\n\n            res = cv2.matchTemplate(i_gray, s_gray, method)\n        w, h = im_search.shape[1], im_search.shape[0]\n\n        result = []\n        while True:\n            min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res)\n            if method in [cv2.TM_SQDIFF, cv2.TM_SQDIFF_NORMED]:\n                top_left = min_loc\n            else:\n                top_left = max_loc\n            if max_val < threshold:\n                break\n            # calculator middle point\n            middle_point = (top_left[0] + w / 2, top_left[1] + h / 2)\n            result.append(dict(\n                result=middle_point,\n                rectangle=(top_left, (top_left[0], top_left[1] + h), (top_left[0] + w, top_left[1]),\n                           (top_left[0] + w, top_left[1] + h)),\n                confidence=max_val\n            ))\n            if maxcnt and len(result) >= maxcnt:\n                break\n            # floodfill the already found area\n            cv2.floodFill(res, None, max_loc, (-1000,), max_val - threshold + 0.1, 1, flags=cv2.FLOODFILL_FIXED_RANGE)\n        return result\n\n    def _sift_instance(self, edge_threshold=100):\n        if hasattr(cv2, 'SIFT'):\n            return cv2.SIFT(edgeThreshold=edge_threshold)\n        return cv2.xfeatures2d.SIFT_create(edgeThreshold=edge_threshold)\n\n    def sift_count(self, img):\n        sift = self._sift_instance()\n        kp, des = sift.detectAndCompute(img, None)\n        return len(kp)\n\n    def find_sift(self, im_source, im_search, min_match_count=4):\n        '''\n        SIFT特征点匹配\n        '''\n        res = self.find_all_sift(im_source, im_search, min_match_count, maxcnt=1)\n        if not res:\n            return None\n        return res[0]\n\n    def find_all_sift(self, im_source, im_search, min_match_count=4, maxcnt=0):\n        '''\n        使用sift算法进行多个相同元素的查找\n        Args:\n            im_source(string): 图像、素材\n            im_search(string): 需要查找的图片\n            threshold: 阈值，当相识度小于该阈值的时候，就忽略掉\n            maxcnt: 限制匹配的数量\n\n        Returns:\n            A tuple of found [(point, rectangle), ...]\n            A tuple of found [{\"point\": point, \"rectangle\": rectangle, \"confidence\": 0.76}, ...]\n            rectangle is a 4 points list\n        '''\n        sift = self._sift_instance()\n        flann = cv2.FlannBasedMatcher({'algorithm': self.FLANN_INDEX_KDTREE, 'trees': 5}, dict(checks=50))\n\n        kp_sch, des_sch = sift.detectAndCompute(im_search, None)\n        if len(kp_sch) < min_match_count:\n            return None\n\n        kp_src, des_src = sift.detectAndCompute(im_source, None)\n        if len(kp_src) < min_match_count:\n            return None\n\n        h, w = im_search.shape[1:]\n\n        result = []\n        while True:\n            # 匹配两个图片中的特征点，k=2表示每个特征点取2个最匹配的点\n            matches = flann.knnMatch(des_sch, des_src, k=2)\n            good = []\n            for m, n in matches:\n                # 剔除掉跟第二匹配太接近的特征点\n                if m.distance < 0.9 * n.distance:\n                    good.append(m)\n\n            if len(good) < min_match_count:\n                break\n\n            sch_pts = np.float32([kp_sch[m.queryIdx].pt for m in good]).reshape(-1, 1, 2)\n            img_pts = np.float32([kp_src[m.trainIdx].pt for m in good]).reshape(-1, 1, 2)\n\n            # M是转化矩阵\n            M, mask = cv2.findHomography(sch_pts, img_pts, cv2.RANSAC, 5.0)\n            matches_mask = mask.ravel().tolist()\n\n            # 计算四个角矩阵变换后的坐标，也就是在大图中的坐标\n            h, w = im_search.shape[:2]\n            pts = np.float32([[0, 0], [0, h - 1], [w - 1, h - 1], [w - 1, 0]]).reshape(-1, 1, 2)\n            dst = cv2.perspectiveTransform(pts, M)\n\n            # trans numpy arrary to python list\n            # [(a, b), (a1, b1), ...]\n            pypts = []\n            for npt in dst.astype(int).tolist():\n                pypts.append(tuple(npt[0]))\n\n            lt, br = pypts[0], pypts[2]\n            middle_point = (lt[0] + br[0]) / 2, (lt[1] + br[1]) / 2\n\n            result.append(dict(\n                result=middle_point,\n                rectangle=pypts,\n                confidence=(matches_mask.count(1), len(good))  # min(1.0 * matches_mask.count(1) / 10, 1.0)\n            ))\n\n            if maxcnt and len(result) >= maxcnt:\n                break\n\n            # 从特征点中删掉那些已经匹配过的, 用于寻找多个目标\n            qindexes, tindexes = [], []\n            for m in good:\n                qindexes.append(m.queryIdx)  # need to remove from kp_sch\n                tindexes.append(m.trainIdx)  # need to remove from kp_img\n\n            def filter_index(indexes, arr):\n                r = np.ndarray(0, np.float32)\n                for i, item in enumerate(arr):\n                    if i not in qindexes:\n                        r = np.append(r, item)\n                return r\n\n            kp_src = filter_index(tindexes, kp_src)\n            des_src = filter_index(tindexes, des_src)\n\n        return result\n\n    def find_all(self, im_source, im_search, maxcnt=0):\n        '''\n        优先Template，之后Sift\n        @ return [(x,y), ...]\n        '''\n        result = self.find_all_template(im_source, im_search, maxcnt=maxcnt)\n        if not result:\n            result = self.find_all_sift(im_source, im_search, maxcnt=maxcnt)\n        if not result:\n            return []\n        return [match[\"result\"] for match in result]\n\n    def find(self, im_source, im_search):\n        '''\n        Only find maximum one object\n        '''\n        r = self.find_all(im_source, im_search, maxcnt=1)\n        return r[0] if r else None\n\n    def brightness(self, im):\n        '''\n        Return the brightness of an image\n        Args:\n            im(numpy): image\n\n        Returns:\n            float, average brightness of an image\n        '''\n        im_hsv = cv2.cvtColor(im, cv2.COLOR_BGR2HSV)\n        h, s, v = cv2.split(im_hsv)\n        height, weight = v.shape[:2]\n        total_bright = 0\n        for i in v:\n            total_bright = total_bright + sum(i)\n        return float(total_bright) / (height * weight)\n\n\nclass Aircv(object):\n    timeout = 30\n    wait_before_operation = 1  # 操作前等待时间 秒\n    rcv_interval = 2  # 接收图片的间隔时间 秒\n    # temporary_directory = \"./\"  # 临时保存截图的目录路径\n    support_network = False  # 是否启用网络下载图片\n    url = \"\"\n    host = \"127.0.0.1:8000\"\n    path = \"/image_service/download/\"\n\n    def __init__(self, d):\n        self.__rcv_interva_time_cache = 0\n\n        self.d = d\n        self.cvHandler = CVHandler()\n        self.FLANN_INDEX_KDTREE = 0\n        # self.aircv_cache_image_name = Aircv.temporary_directory + self.d._host + \"_aircv_cache_image.jpg\"\n        self.debug = True\n        self.aircv_cache_image = None\n        self.ws_screen = None\n        self.zoom_out = None\n        # 下面三个函数放在最后，而且顺序不能变\n        self.detection_screen()\n        self.start_get_screen()\n        self.get_scaling_ratio()\n\n    def detection_screen(self):\n        \"\"\"检测设备屏幕比例，必须为 16:9\"\"\"\n        display_height = self.d.info['displayHeight']\n        display_width = self.d.info['displayWidth']\n        if display_height / display_width != 16 / 9 and display_width / display_height != 16 / 9:\n            raise RuntimeError(\"Does not support current mobile phones, The screen ratio is not 16:9\")\n\n    def get_scaling_ratio(self):\n        \"\"\"计算缩放比\"\"\"\n        while True:\n            if self.aircv_cache_image is not None:\n                self.zoom_out = 1.0 * self.d.info['displayHeight'] / self.aircv_cache_image.shape[0]\n                break\n\n    def start_get_screen(self):\n\n        def on_message(ws, message):\n            this = self\n            if isinstance(message, bytes):\n\n                if int(time.time()) - this.__rcv_interva_time_cache >= Aircv.rcv_interval:\n                    # with open(this.aircv_cache_image_name, 'wb') as f:\n                    #     f.write(message)\n                    # this.aircv_cache_image = this.cvHandler.imread(self.aircv_cache_image_name)\n                    this.aircv_cache_image = this.cvHandler.imdecode(message)\n                    this.__rcv_interva_time_cache = int(time.time())\n\n        def on_error(ws, error):\n            raise RuntimeError(error)\n\n        def on_close(ws):\n            print(\"### ws_screen closed ###\")\n\n        def on_open(ws):\n            print(\"### ws_screen on_open ###\")\n\n        if not self.ws_screen or not self.ws_screen.keep_running:\n            self.ws_screen = websocket.WebSocketApp(\"ws://\" + self.d._host + \":\" + str(self.d._port) + \"/minicap\",\n                                                    on_open=on_open,\n                                                    on_message=on_message,\n                                                    on_error=on_error,\n                                                    on_close=on_close)\n            ws_thread = threading.Thread(target=self.ws_screen.run_forever)\n            ws_thread.daemon = True\n            ws_thread.start()\n\n    def stop_get_scren(self):\n        if self.ws_screen and self.ws_screen.keep_running:\n            self.ws_screen.close()\n\n    # operating\n    def find_template_by_crop(self, img, area=None):\n        if Aircv.support_network:\n            img_url = \"\".join([\"http://\", Aircv.host, Aircv.path, img])\n            data = requests.get(img_url)\n            img_serch = self.cvHandler.imdecode(data.content)\n        else:\n            img_serch = self.cvHandler.imread(img)\n        if area:\n            crop_img = self.aircv_cache_image[area[1]:area[3], area[0]:area[2]]\n            result = self.cvHandler.find_template(crop_img, img_serch)\n            point = result['result'] if result else None\n            if point:\n                point = (point[0] + area[0], point[1] + area[1])\n        else:\n            crop_img = self.aircv_cache_image\n            result = self.cvHandler.find_template(crop_img, img_serch)\n            point = result['result'] if result else None\n\n        return (int(point[0] * self.zoom_out), int(point[1] * self.zoom_out)) if point else None\n\n    def exists(self, img, timeout=timeout, area=None):\n        point = None\n        is_exists = False\n        while timeout:\n            if self.debug:\n                print(timeout)\n            if self.aircv_cache_image is not None:\n                point = self.find_template_by_crop(img, area)\n            if point:\n                is_exists = True\n                break\n            else:\n                timeout -= 1\n                time.sleep(1)\n        return is_exists\n\n    def click(self, img, timeout=timeout, area=None):\n        point = None\n        while timeout:\n            if self.debug:\n                print(timeout)\n            if self.aircv_cache_image is not None:\n                point = self.find_template_by_crop(img, area)\n            if point:\n                time.sleep(Aircv.wait_before_operation)\n                self.d.click(point[0], point[1])\n                break\n            else:\n                timeout -= 1\n                time.sleep(1)\n            if not timeout:\n                raise RuntimeError('No image found')\n\n    def click_index(self, img, index=1, maxcnt=20, timeout=timeout):\n        point = None\n        img_serch = self.cvHandler.imread(img)\n        while timeout:\n            if self.debug:\n                print(timeout)\n            if self.aircv_cache_image is not None:\n                result_list = self.cvHandler.find_all_template(self.aircv_cache_image, img_serch, maxcnt=maxcnt)\n                point = result_list[index - 1]['result'] if result_list else None\n            if point:\n                time.sleep(Aircv.wait_before_operation)\n                self.d.click(point[0], point[1])\n                break\n            else:\n                timeout -= 1\n                time.sleep(1)\n            if not timeout:\n                raise RuntimeError('No image found')\n\n    def long_click(self, img, duration=None, timeout=timeout, area=None):\n        point = None\n        while timeout:\n            if self.debug:\n                print(timeout)\n            if self.aircv_cache_image is not None:\n                point = self.find_template_by_crop(img, area)\n            if point:\n                time.sleep(Aircv.wait_before_operation)\n                self.d.long_click(point[0], point[1], duration)\n                break\n            else:\n                timeout -= 1\n                time.sleep(1)\n            if not timeout:\n                raise RuntimeError('No image found')\n\n    def swipe(self, img_from, img_to, duration=0.1, steps=None, timeout=timeout, area=None):\n        point_from = None\n        point_to = None\n        while timeout:\n            if self.debug:\n                print(timeout)\n            if self.aircv_cache_image is not None:\n                point_from = self.find_template_by_crop(img_from, area)\n                point_to = self.find_template_by_crop(img_to, area)\n            if point_from and point_to:\n                time.sleep(Aircv.wait_before_operation)\n                self.d.swipe(point_from[0], point_from[1], point_to[0], point_to[1], duration, steps)\n                break\n            else:\n                timeout -= 1\n                time.sleep(1)\n            if not timeout:\n                raise RuntimeError('No image found')\n\n    def swipe_points(self, img_list, duration=0.5, timeout=timeout, area=None):\n        point_list = []\n        while timeout:\n            if self.debug:\n                print(timeout)\n            if self.aircv_cache_image is not None:\n                for img in img_list:\n                    point = self.find_template_by_crop(img, area)\n                    if not point:\n                        break\n                    point_list.append(point)\n            if len(point_list) == len(img_list):\n                time.sleep(Aircv.wait_before_operation)\n                self.d.swipe_points(point_list, duration)\n                break\n            else:\n                timeout -= 1\n                time.sleep(1)\n            if not timeout:\n                raise RuntimeError('No image found')\n\n    def drag(self, img_from, img_to, duration=0.1, steps=None, timeout=timeout, area=None):\n        point_from = None\n        point_to = None\n        while timeout:\n            if self.debug:\n                print(timeout)\n            if self.aircv_cache_image is not None:\n                point_from = self.find_template_by_crop(img_from, area)\n                point_to = self.find_template_by_crop(img_to, area)\n            if point_from and point_to:\n                time.sleep(Aircv.wait_before_operation)\n                self.d.drag(point_from[0], point_from[1], point_to[0], point_to[1], duration, steps)\n                break\n            else:\n                timeout -= 1\n                time.sleep(1)\n            if not timeout:\n                raise RuntimeError('No image found')\n\n    def get_point(self, img, timeout=timeout, area=None):\n        point = None\n        while timeout:\n            if self.debug:\n                print(timeout)\n            if self.aircv_cache_image is not None:\n                point = self.find_template_by_crop(img, area)\n            if point:\n                break\n            else:\n                timeout -= 1\n                time.sleep(1)\n            if not timeout:\n                raise RuntimeError('No image found')\n        return point\n\n\n\n"
  },
  {
    "path": "_archived/init.py",
    "content": "# coding: utf-8\n#\n\nimport datetime\nimport hashlib\nimport logging\nimport os\nimport shutil\nimport tarfile\nfrom pathlib import Path\n\nimport adbutils\nimport progress.bar\nimport requests\nfrom retry import retry\n\nfrom uiautomator2.utils import natualsize\nfrom uiautomator2.version import __apk_version__, __atx_agent_version__, __jar_version__, __version__\n\nappdir = os.path.join(os.path.expanduser(\"~\"), '.uiautomator2')\n\nGITHUB_BASEURL = \"https://github.com/openatx\"\n\n\nlogger = logging.getLogger(__name__)\nassets_dir = Path(__file__).absolute().parent.joinpath(\"assets\")\n\nclass DownloadBar(progress.bar.PixelBar):\n    message = \"Downloading\"\n    suffix = '%(current_size)s/%(total_size)s'\n    width = 10\n\n    @property\n    def total_size(self):\n        return natualsize(self.max)\n\n    @property\n    def current_size(self):\n        return natualsize(self.index)\n\n\ndef gen_cachepath(url: str) -> str:\n    filename = os.path.basename(url)\n    storepath = os.path.join(\n        appdir, \"cache\",\n        filename.replace(\" \", \"_\") + \"-\" +\n        hashlib.sha224(url.encode()).hexdigest()[:10], filename)\n    return storepath\n\ndef cache_download(url, filename=None, timeout=None, storepath=None, logger=logger):\n    \"\"\" return downloaded filepath \"\"\"\n    # check cache\n    if not filename:\n        filename = os.path.basename(url)\n    if not storepath:\n        storepath = gen_cachepath(url)\n    storedir = os.path.dirname(storepath)\n    if not os.path.isdir(storedir):\n        os.makedirs(storedir)\n    if os.path.exists(storepath) and os.path.getsize(storepath) > 0:\n        logger.debug(\"Use cached assets: %s\", storepath)\n        return storepath\n\n    logger.debug(\"Download %s\", url)\n    # download from url\n    headers = {\n        'Accept': '*/*',\n        'Accept-Encoding': 'gzip, deflate, br',\n        'Accept-Language': 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2',\n        'Connection': 'keep-alive',\n        'Origin': 'https://github.com',\n        'User-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36'\n    } # yapf: disable\n    r = requests.get(url, stream=True, headers=headers, timeout=None)\n    r.raise_for_status()\n\n    file_size = int(r.headers.get(\"Content-Length\"))\n    bar = DownloadBar(filename, max=file_size)\n    with open(storepath + '.part', 'wb') as f:\n        chunk_length = 16 * 1024\n        while 1:\n            buf = r.raw.read(chunk_length)\n            if not buf:\n                break\n            f.write(buf)\n            bar.next(len(buf))\n        bar.finish()\n\n    assert file_size == os.path.getsize(storepath +\n                                        \".part\")  # may raise FileNotFoundError\n    shutil.move(storepath + '.part', storepath)\n    return storepath\n\ndef mirror_download(url: str, filename=None):\n    \"\"\"\n    Download from mirror, then fallback to origin url\n    \"\"\"\n    storepath = gen_cachepath(url)\n    if not filename:\n        filename = os.path.basename(url)\n    github_host = \"https://github.com\"\n    if url.startswith(github_host):\n        mirror_url = \"https://tool.appetizer.io\" + url[len(\n            github_host):]  # mirror of github\n        try:\n            return cache_download(mirror_url,\n                                  filename,\n                                  timeout=60,\n                                  storepath=storepath,\n                                  logger=logger)\n        except (requests.RequestException, FileNotFoundError,\n                AssertionError) as e:\n            logger.debug(\"download error from mirror(%s), use origin source\", e)\n\n    return cache_download(url, filename, storepath=storepath, logger=logger)\n\n\ndef app_uiautomator_apk_urls():\n    ret = []\n    for name in [\"app-uiautomator.apk\", \"app-uiautomator-test.apk\"]:\n        ret.append((name, \"\".join([\n            GITHUB_BASEURL, \"/android-uiautomator-server/releases/download/\",\n            __apk_version__, \"/\", name\n        ])))\n    return ret\n\n\ndef parse_apk(path: str):\n    \"\"\"\n    Parse APK\n    \n    Returns:\n        dict contains \"package\" and \"main_activity\"\n    \"\"\"\n    import apkutils2\n    apk = apkutils2.APK(path)\n    package_name = apk.manifest.package_name\n    main_activity = apk.manifest.main_activity\n    return {\n        \"package\": package_name,\n        \"main_activity\": main_activity,\n    }\n\nclass Initer():\n    def __init__(self, device: adbutils.AdbDevice, loglevel=logging.DEBUG):\n        d = self._device = device\n\n        self.sdk = d.getprop('ro.build.version.sdk')\n        self.abi = d.getprop('ro.product.cpu.abi')\n        self.pre = d.getprop('ro.build.version.preview_sdk')\n        self.arch = d.getprop('ro.arch')\n        self.abis = (d.getprop('ro.product.cpu.abilist').strip()\n                     or self.abi).split(\",\")\n        \n        self.__atx_listen_addr = \"127.0.0.1:7912\"\n        logger.info(\"uiautomator2 version: %s\", __version__)\n\n    def set_atx_agent_addr(self, addr: str):\n        assert \":\" in addr\n        self.__atx_listen_addr = addr\n\n    @property\n    def atx_agent_path(self):\n        return \"/data/local/tmp/atx-agent\"\n\n    def shell(self, *args, timeout=60):\n        logger.debug(\"Shell: %s\", args)\n        return self._device.shell(args, timeout=60)\n\n    @property\n    def jar_urls(self):\n        \"\"\"\n        Returns:\n            iter([name, url], [name, url])\n        \"\"\"\n        for name in ['bundle.jar', 'uiautomator-stub.jar']:\n            yield (name, \"\".join([\n                GITHUB_BASEURL,\n                \"/android-uiautomator-jsonrpcserver/releases/download/\",\n                __jar_version__, \"/\", name\n            ]))\n\n    @property\n    def atx_agent_url(self):\n        files = {\n            'armeabi-v7a': 'atx-agent_{v}_linux_armv7.tar.gz',\n            'arm64-v8a': 'atx-agent_{v}_linux_arm64.tar.gz',\n            'armeabi': 'atx-agent_{v}_linux_armv6.tar.gz',\n            'x86': 'atx-agent_{v}_linux_386.tar.gz',\n            'x86_64': 'atx-agent_{v}_linux_386.tar.gz',\n        }\n        name = None\n        for abi in self.abis:\n            name = files.get(abi)\n            if name:\n                break\n        if not name:\n            raise Exception(\n                \"arch(%s) need to be supported yet, please report an issue in github\"\n                % self.abis)\n        return GITHUB_BASEURL + '/atx-agent/releases/download/%s/%s' % (\n            __atx_agent_version__, name.format(v=__atx_agent_version__))\n\n    @property\n    def minicap_urls(self):\n        \"\"\"\n        binary from https://github.com/openatx/stf-binaries\n        only got abi: armeabi-v7a and arm64-v8a\n        \"\"\"\n        base_url = GITHUB_BASEURL + \\\n            \"/stf-binaries/raw/0.3.0/node_modules/@devicefarmer/minicap-prebuilt/prebuilt/\"\n        sdk = self.sdk\n        yield base_url + self.abi + \"/lib/android-\" + sdk + \"/minicap.so\"\n        yield base_url + self.abi + \"/bin/minicap\"\n\n    @property\n    def minitouch_url(self):\n        return ''.join([\n            GITHUB_BASEURL + \"/stf-binaries\",\n            \"/raw/0.3.0/node_modules/@devicefarmer/minitouch-prebuilt/prebuilt/\",\n            self.abi + \"/bin/minitouch\"\n        ])\n\n    @retry(tries=2, logger=logger)\n    def push_url(self, url, dest=None, mode=0o755, tgz=False, extract_name=None):  # yapf: disable\n        path = mirror_download(url, filename=os.path.basename(url))\n        if tgz:\n            tar = tarfile.open(path, 'r:gz')\n            path = os.path.join(os.path.dirname(path), extract_name)\n            tar.extract(extract_name,\n                        os.path.dirname(path))  # zlib.error may raise\n\n        if not dest:\n            dest = \"/data/local/tmp/\" + os.path.basename(path)\n\n        logger.debug(\"Push to %s:0%o\", dest, mode)\n        self._device.sync.push(path, dest, mode=mode)\n        return dest\n\n    def is_apk_outdated(self):\n        \"\"\"\n        If apk signature mismatch, the uiautomator test will fail to start\n        command: am instrument -w -r -e debug false \\\n                -e class com.github.uiautomator.stub.Stub \\\n                com.github.uiautomator.test/android.support.test.runner.AndroidJUnitRunner\n        java.lang.SecurityException: Permission Denial: \\\n            starting instrumentation ComponentInfo{com.github.uiautomator.test/android.support.test.runner.AndroidJUnitRunner} \\\n            from pid=7877, uid=7877 not allowed \\\n            because package com.github.uiautomator.test does not have a signature matching the target com.github.uiautomator\n        \"\"\"\n        apk_debug = self._device.package_info(\"com.github.uiautomator\")\n        apk_debug_test = self._device.package_info(\n            \"com.github.uiautomator.test\")\n        logger.debug(\"apk-debug package-info: %s\", apk_debug)\n        logger.debug(\"apk-debug-test package-info: %s\", apk_debug_test)\n        if not apk_debug or not apk_debug_test:\n            return True\n        if apk_debug['version_name'] != __apk_version__:\n            logger.info(\n                \"package com.github.uiautomator version %s, latest %s\",\n                apk_debug['version_name'], __apk_version__)\n            return True\n\n        if apk_debug['signature'] != apk_debug_test['signature']:\n            # On vivo-Y67 signature might not same, but signature matched.\n            # So here need to check first_install_time again\n            max_delta = datetime.timedelta(minutes=3)\n            if abs(apk_debug['first_install_time'] -\n                   apk_debug_test['first_install_time']) > max_delta:\n                logger.debug(\n                    \"package com.github.uiautomator does not have a signature matching the target com.github.uiautomator\"\n                )\n                return True\n        return False\n\n    def is_atx_agent_outdated(self):\n        \"\"\"\n        Returns:\n            bool\n        \"\"\"\n        agent_version = self._device.shell([self.atx_agent_path, \"version\"]).strip()\n        if agent_version == \"dev\":\n            logger.info(\"skip version check for atx-agent dev\")\n            return False\n\n        # semver major.minor.patch\n        try:\n            real_ver = list(map(int, agent_version.split(\".\")))\n            want_ver = list(map(int, __atx_agent_version__.split(\".\")))\n        except ValueError:\n            return True\n\n        logger.debug(\"Real version: %s, Expect version: %s\", real_ver,\n                          want_ver)\n\n        if real_ver[:2] != want_ver[:2]:\n            return True\n\n        return real_ver[2] < want_ver[2]\n\n    def check_install(self):\n        \"\"\"\n        Only check atx-agent and test apks (Do not check minicap and minitouch)\n\n        Returns:\n            True if everything is fine, else False\n        \"\"\"\n        d = self._device\n        if d.sync.stat(self.atx_agent_path).size == 0:\n            return False\n\n        if self.is_atx_agent_outdated():\n            return False\n\n        if self.is_apk_outdated():\n            return False\n\n        return True\n\n    def _install_uiautomator_apks(self):\n        \"\"\" use uiautomator 2.0 to run uiautomator test\n        通常在连接USB数据线的情况下调用\n        \"\"\"\n        self.shell(\"pm\", \"uninstall\", \"com.github.uiautomator\")\n        self.shell(\"pm\", \"uninstall\", \"com.github.uiautomator.test\")\n        for filename, url in app_uiautomator_apk_urls():\n            path = self.push_url(url, mode=0o644)\n            self.shell(\"pm\", \"install\", \"-r\", \"-t\", path)\n            logger.info(\"- %s installed\", filename)\n\n    def _install_jars(self):\n        \"\"\" use uiautomator 1.0 to run uiautomator test \"\"\"\n        for (name, url) in self.jar_urls:\n            self.push_url(url, \"/data/local/tmp/\" + name, mode=0o644)\n\n    def _install_atx_agent(self):\n        logger.info(\"Install atx-agent %s\", __atx_agent_version__)\n        if 'armeabi' in self.abis:\n            local_atx_agent_path = assets_dir.joinpath(\"atx-agent\")\n            if local_atx_agent_path.exists():\n                logger.info(\"Use local atx-agent[armeabi]: %s\", local_atx_agent_path)\n                dest = '/data/local/tmp/atx-agent'\n                self._device.sync.push(local_atx_agent_path, dest, mode=0o755)\n                return\n        self.push_url(self.atx_agent_url, tgz=True, extract_name=\"atx-agent\")\n\n    def setup_atx_agent(self):\n        # stop atx-agent first\n        self.shell(self.atx_agent_path, \"server\", \"--stop\")\n        if self.is_atx_agent_outdated():\n            self._install_atx_agent()\n        \n        self.shell(self.atx_agent_path, 'server', '--nouia', '-d', \"--addr\", self.__atx_listen_addr)\n        logger.info(\"Check atx-agent version\")\n        self.check_atx_agent_version()\n\n    @retry(\n        (requests.ConnectionError, requests.ReadTimeout, requests.HTTPError),\n        delay=.5,\n        tries=10)\n    def check_atx_agent_version(self):\n        port = self._device.forward_port(7912)\n        logger.debug(\"Forward: local:tcp:%d -> remote:tcp:%d\", port, 7912)\n        version = requests.get(\"http://%s:%d/version\" %\n                               (self._device._client.host, port)).text.strip()\n        logger.debug(\"atx-agent version %s\", version)\n\n        wlan_ip = requests.get(\"http://%s:%d/wlan/ip\" %\n                               (self._device._client.host, port)).text.strip()\n        logger.debug(\"device wlan ip: %s\", wlan_ip)\n        return version\n\n    def install(self):\n        \"\"\"\n        TODO: push minicap and minitouch from tgz file\n        \"\"\"\n        logger.info(\"Install minicap, minitouch\")\n        self.push_url(self.minitouch_url)\n        if self.abi == \"x86\":\n            logger.info(\n                \"abi:x86 not supported well, skip install minicap\")\n        elif int(self.sdk) > 30:\n            logger.info(\"Android R (sdk:30) has no minicap resource\")\n        else:\n            for url in self.minicap_urls:\n                self.push_url(url)\n\n        # self._install_jars() # disable jars\n        if self.is_apk_outdated():\n            logger.info(\n                \"Install com.github.uiautomator, com.github.uiautomator.test %s\",\n                __apk_version__)\n            self._install_uiautomator_apks()\n        else:\n            logger.info(\"Already installed com.github.uiautomator apks\")\n\n        self.setup_atx_agent()\n        print(\"Successfully init %s\" % self._device)\n\n    def uninstall(self):\n        self._device.shell([self.atx_agent_path, \"server\", \"--stop\"])\n        self._device.shell([\"rm\", self.atx_agent_path])\n        logger.info(\"atx-agent stopped and removed\")\n        self._device.shell([\"rm\", \"/data/local/tmp/minicap\"])\n        self._device.shell([\"rm\", \"/data/local/tmp/minicap.so\"])\n        self._device.shell([\"rm\", \"/data/local/tmp/minitouch\"])\n        logger.info(\"minicap, minitouch removed\")\n        self._device.shell([\"pm\", \"uninstall\", \"com.github.uiautomator\"])\n        self._device.shell([\"pm\", \"uninstall\", \"com.github.uiautomator.test\"])\n        logger.info(\"com.github.uiautomator uninstalled, all done !!!\")\n\n\nif __name__ == \"__main__\":\n    import adbutils\n\n    serial = None\n    device = adbutils.adb.device(serial)\n    init = Initer(device, loglevel=logging.DEBUG)\n    print(init.check_install())\n"
  },
  {
    "path": "_archived/messagebox.py",
    "content": "# coding: utf-8\n#\n\nimport time\n\ntry:\n    import Tkinter as tk\nexcept ImportError:\n    import tkinter as tk\n\n\ndef retryskipabort(message, timeout=20):\n    \"\"\"\n    Show dialog of RETRY,SKIP,ABORT\n    Returns:\n        one of \"retry\", \"skip\", \"abort\"\n    \"\"\"\n    root = tk.Tk()\n    root.geometry(\"400x200\")\n    root.title(\"Exception handle\")\n    root.eval('tk::PlaceWindow %s center' % root.winfo_pathname(root.winfo_id()))\n    root.attributes(\"-topmost\", True)\n\n    _kvs = {\"result\": \"abort\"}\n\n    def cancel_timer(*args):\n        root.after_cancel(_kvs['root'])\n        root.title(\"Manual\")\n\n    def update_prompt():\n        cancel_timer()\n\n    def f(result):\n        def _inner():\n            _kvs['result'] = result\n            root.destroy()\n        return _inner\n\n    tk.Label(root, text=message).pack(side=tk.TOP, fill=tk.X, pady=10)\n\n    frmbtns = tk.Frame(root)\n    tk.Button(frmbtns, text=\"Skip\", command=f('skip')).pack(side=tk.LEFT)\n    tk.Button(frmbtns, text=\"Retry\", command=f('retry')).pack(side=tk.LEFT)\n    tk.Button(frmbtns, text=\"ABORT\", command=f('abort')).pack(side=tk.LEFT)\n    frmbtns.pack(side=tk.BOTTOM)\n\n    prompt = tk.StringVar()\n    label1 = tk.Label(root, textvariable=prompt) #, width=len(prompt))\n    label1.pack()\n\n    deadline = time.time() + timeout\n\n    def _refresh_timer():\n        leftseconds = deadline - time.time()\n        if leftseconds <= 0:\n            root.destroy()\n            return\n        root.title(\"Test will stop after \" + str(int(leftseconds)) + \" s\")\n        _kvs['root'] = root.after(500, _refresh_timer)\n\n    _kvs['root'] = root.after(0, _refresh_timer)\n    root.bind('<Button-1>', cancel_timer)\n    root.mainloop()\n\n    return _kvs['result']\n\n\nif __name__ == '__main__':\n    print(retryskipabort('LKJSDF\\nlkjj\\what?lkjsdlfjaskdfjlasdkjflnice'))\n"
  },
  {
    "path": "_archived/ocr/README.md",
    "content": "# 使用百度OCR选取文字元素\n\n## 前提条件\n\n1.需要有百度云账号，百度云注册账号: https://cloud.baidu.com/?from=console\n\n2.创建一个文字识别的应用: https://console.bce.baidu.com/ai/#/ai/ocr/overview/index \n\n  记住三个值 AppID 、API_Key、Secret_Key\n\n3.需要安装百度OCR Python SDK：`pip install baidu-aip`\n\n百度OCR具体应用见百度文档：https://cloud.baidu.com/doc/OCR/s/ejwvxzls6\n\n## 示例\n\n```python\nimport uiautomator2 as u2\nimport uiautomator2.ext.ocr.baiduOCR as ocr\n\nAPP_ID = '创建应用的APP_ID'\nAPI_KEY = '创建应用的API_KEY'\nSECRECT_KEY = '创建应用的SECRECT_KEY'\n# options = {\"templateSign\": ''}  # iOCR财会票据识别模板id\n\nu2.plugin_add(\"ocr\", ocr.OCR, APP_ID, API_KEY, SECRECT_KEY)\n# u2.plugin_add(\"ocrCustom\", ocr.OCRCustom, APP_ID, API_KEY, SECRECT_KEY, options)\n\nd = u2.connect()\nd.ext_ocr(\"对战模式\").click()\n# d.ext_ocrCustom(\"对战模式\").click()\n```"
  },
  {
    "path": "_archived/ocr/__init__.py",
    "content": "# coding: utf-8\n#\n\"\"\"\nimport uiautomator2 as u2\nimport uiautomator2.ext.ocr as ocr\n\nu2.plugin_add(\"ocr\", ocr.OCR)\n\nd = u2.connect()\nd.ext_ocr(\"对战模式\").click()\n\"\"\"\n\nimport time\n\nimport requests\n\nAPI = \"\"\n\n\nclass OCRObjectNotFound(Exception):\n    pass\n\n\nclass OCR(object):\n    def __init__(self, d):\n        \"\"\"\n        Args:\n            d: uiautomator2 instance\n        \"\"\"\n        self._d = d\n        if not API:\n            raise EnvironmentError(\"set API var before using OCR\")\n\n    def all(self):\n        rawdata = self._d.screenshot(format='raw')\n        r = requests.post(API, files={\"file\": (\"tmp.jpg\", rawdata)})\n        r.raise_for_status()\n        resp = r.json()\n        assert resp['success']\n        result = []\n        for item in resp['data']:\n            lx, ly, rx, ry = item['coords']\n            x, y = (lx + rx) // 2, (ly + ry) // 2\n            ocr_text = item['text']\n            result.append((ocr_text, x, y))\n        result.sort(key=lambda v: (v[2], v[1]))\n        return result\n\n    def __call__(self, text):\n        return OCRSelector(self, text)\n\n\nclass OCRSelector(object):\n    def __init__(self, server, text=None, textContains=None):\n        self._server = server\n        self._d = server._d\n        self._text = text\n        self._text_contains = textContains\n\n    def all(self):\n        result = []\n        for (ocr_text, x, y) in self._server.all():\n            matched = False\n            if self._text == ocr_text:  # exactly match\n                matched = True\n            elif self._text_contains and self._text_contains in ocr_text:\n                matched = True\n            if matched:\n                result.append((ocr_text, x, y))\n        return result\n\n    def wait(self, timeout=10):\n        \"\"\"\n        Args:\n            timeout: seconds to wait\n        \n        Returns:\n            List of recognition (text, x, y)\n            \n        Raises:\n            OCRObjectNotFound\n        \"\"\"\n        deadline = time.time() + timeout\n        first = True\n        while first or time.time() < deadline:\n            first = False\n            all = self.all()\n            if all:\n                return all\n        raise OCRObjectNotFound(self._text)\n\n    def click(self, timeout=10):\n        result = self.wait(timeout=timeout)\n        _, x, y = result[0]\n        self._d.click(x, y)\n\n\nif __name__ == '__main__':\n    import uiautomator2 as u2\n    import uiautomator2.ext.ocr as ocr\n\n    d = u2.connect()\n    print(ocr.OCR(d)(\"王者峡谷\").click())"
  },
  {
    "path": "_archived/ocr/baiduOCR.py",
    "content": "#!/usr/bin/env python3\n\n\"\"\"\n@version: 1.0.0\n@author: rainy008\n@description: 使用百度OCR实现截屏选取元素\n\"\"\"\n\nfrom aip import AipOcr\n\nfrom uiautomator2.ext.ocr import OCR as u2OCR\nfrom uiautomator2.ext.ocr import OCRSelector as u2OCRSelector\n\n\nclass OCR(u2OCR):\n    def __init__(self, d, app_id, api_key, secrect_key):\n        self._d = d\n        self._APP_ID = app_id\n        self._API_KEY = api_key\n        self._SECRECT_KEY = secrect_key\n        self._client = AipOcr(self._APP_ID, self._API_KEY, self._SECRECT_KEY)\n\n    def all(self):\n        img = self._d.screenshot(format='raw')\n        resp = self._client.general(img)  # 通用文字识别(含位置信息版)，每天 500 次免费\n        result = []\n        for item in resp['words_result']:\n            left = item['location'].get('left')\n            top = item['location'].get('top')\n            width = item['location'].get('width')\n            height = item['location'].get('height')\n            x, y = left + width // 2, top + height // 2\n            ocr_text = item['words']\n            result.append((ocr_text, x, y))\n        result.sort(key=lambda v: (v[2], v[1]))\n        # print(result)\n        return result\n\n    def __call__(self, text, exact=True):\n        return OCRSelector(self, text, exact)\n\n\nclass OCRSelector(u2OCRSelector):\n    def __init__(self, server, text, exact=True):\n        self._server = server\n        self._d = server._d\n        self._text = text\n        self._exact = exact\n\n    def all(self):\n        result = []\n        for (ocr_text, x, y) in self._server.all():\n            if self._exact and self._text == ocr_text:  # exactly match\n                result.append((ocr_text, x, y))\n            elif self._text in ocr_text:\n                result.append((ocr_text, x, y))\n        return result\n\n    def get_text(self, timeout=10):\n        result = self.wait(timeout=timeout)\n        word = result[0][0]\n        return word\n\n\nclass OCRCustom(OCR):\n    def __init__(self, d, app_id, api_key, secrect_key, options):\n        super(OCRCustom, self).__init__(d, app_id, api_key, secrect_key)\n        self.options = options\n\n    def get_words(self):\n        img = self._d.screenshot(format='raw')\n        resp = self._client.custom(img, self.options)  # iocr财会票据文字识别(含位置信息版)，每天 500 次免费\n        return resp\n\n    def all(self):\n        resp = self.get_words()\n        result = []\n        for item in resp['data']['ret']:\n            left = item['location'].get('left')\n            top = item['location'].get('top')\n            width = item['location'].get('width')\n            height = item['location'].get('height')\n            x, y = left + width // 2, top + height // 2\n            ocr_text = item['word']\n            ocr_text_name = item['word_name']\n            result.append((ocr_text, x, y))\n            result.append((ocr_text_name, x, y))\n        result.sort(key=lambda v: (v[2], v[1]))\n        # print(result)\n        return result\n\n    def get(self, option):\n        \"\"\"\n        返回自定义字段的值\n        :param option: 自定义的字段，现仅有score和name\n        :return:\n        \"\"\"\n        resp = self.get_words()\n        for item in resp['data']['ret']:\n            if item['word_name'] == option:\n                return item['word']\n\n"
  },
  {
    "path": "_archived/webview.py",
    "content": "# coding: utf-8\n#\n# Not implemented yet.\n#\nimport json\nimport logging\nimport string\nfrom pprint import pprint\n\nimport adbutils\nimport pychrome\nimport requests\n\nlogger = logging.getLogger(__name__)\n\nclass WebviewDriver():\n    def __init__(self, url):\n        self._url = url\n        self._browser = pychrome.Browser(self._url)\n\n    @property\n    def browser(self):\n        \"\"\" new Browser all the time to clear history data \"\"\"\n        return self._browser\n\n    def get_active_tab_list(self):\n        tabs = []\n        for tab in self.browser.list_tab():\n            logger.debug(\"tab: %s\", tab)\n            tab.start()\n            t = BrowserTab(tab)\n            if t.is_activate():\n                tabs.append(t)\n            else:\n                tab.stop()\n        return tabs\n    \n    def get_activate_tab(self):\n        pass\n\n\nclass BrowserTab():\n    def __init__(self, tab):\n        self._tab = tab\n        # I donot know why should call, Runtime.enable() ..., as I know, chromedriver call that.\n        # self._call(\"Runtime.enable\")\n        # self._call(\"Page.enable\")\n\n        self._evaluate(\"_C = {}\")\n\n    def is_activate(self):\n        \"\"\" is page activate \"\"\"\n        height = self._evaluate(\"window.innerHeight\")\n        hidden = self._evaluate(\"document.hidden\")\n        return not hidden and height > 0\n\n    def close(self):\n        self._tab.stop()\n    \n    def _evaluate(self, expression, **kwargs):\n        if kwargs:\n            d = {}\n            for k, v in kwargs.items():\n                d[k] = json.dumps(v)\n            t = string.Template(expression)\n            expression = t.substitute(d)\n        return self._call(\"Runtime.evaluate\", expression=expression)\n\n    def _call(self, method, **kwargs):\n        logger.debug(\"call: %s, kwargs: %s\", method, kwargs)\n        response = self._tab.call_method(method, **kwargs)\n        logger.debug(\"response: %s\", response)\n        return response.get('result', {}).get('value')\n\n    def current_url(self):\n        return self._evaluate(\"window.location.href\")\n\n    def set_current_url(self, url: str):\n        return self._evaluate(\"\"\"(function(url) {\n            window.location.href = ${url}\n        })\"\"\", url=url)\n\n    def find_element_by_xpath(self, xpath: str):\n        self._evaluate('''(function(xpath){\n            var obj = document.evaluate(xpath, document, null, XPathResult.ANY_TYPE, null);\n            var button = obj.iterateNext();\n            _C[1] = button;\n        })($xpath)\n        ''')\n\n    def coord_by_xpath(self, xpath: str):\n        coord = self._evaluate('''(function(xpath){\n            var obj = document.evaluate(xpath, document, null, XPathResult.ANY_TYPE, null);\n            var button = obj.iterateNext();\n            var rect = button.getBoundingClientRect()\n            // [rect.left, rect.top, rect.right, rect.bottom]\n            var x = (rect.left + rect.right)/2\n            var y = (rect.top + rect.bottom)/2;\n            return JSON.stringify([x, y])\n        })(${xpath})''', xpath=xpath)\n        return json.loads(coord)\n    \n    def click(self, x, y, duration=0.2, tap_count=1):\n        mills = int(1000*duration) # convert to ms\n        self._call(\"Input.synthesizeTapGesture\", x=x, y=y, duration=mills, tapCount=tap_count)\n\n\n    def click_by_xpath(self, xpath):\n        x, y = self.coord_by_xpath(xpath)\n        self.click(x, y)\n\n    def clear_text_by_xpath(self, xpath):\n        self._evaluate(\"\"\"(function(xpath){\n            var obj = document.evaluate(xpath, document, null, XPathResult.ANY_TYPE, null);\n            var button = obj.iterateNext();\n            button.value = \"\"\n        })($xpath)\"\"\", xpath=xpath)\n\n    def send_keys(self, text):\n        \"\"\"\n        Input text\n\n        Refs:\n            https://github.com/Tencent/FAutoTest/blob/58766fcb98d135ebb6be88893d10c789a1a50e18/fastAutoTest/core/h5/h5PageOperator.py#L40\n            http://compatibility.remotedebug.org/Input/Chrome%20(CDP%201.2)/commands/dispatchKeyEvent\n        \"\"\"\n        for c in text:\n            self._call(\"Input.dispatchKeyEvent\", type=\"char\", text=c)\n\n    def screenshot(self):\n        \"\"\" always stuck \"\"\"\n        raise NotImplementedError()\n\n\nfrom contextlib import contextmanager\n\nfrom selenium import webdriver\n\n\n@contextmanager\ndef driver(package_name):\n    serial = adbutils.adb.device().serial\n    capabilities = {\n        \"androidDeviceSerial\": serial,\n        \"androidPackage\": package_name,\n        \"androidUseRunningApp\": True,\n    }\n    dr = webdriver.Remote(\"http://localhost:9515\", {\n        \"chromeOptions\": capabilities\n    })\n    try:\n        yield dr\n    finally:\n        dr.quit()\n\ndef chromedriver():\n    package_name = \"io.appium.android.apis\"\n    package_name = \"com.xueqiu.android\"\n\n    with driver(package_name) as dr:\n        print(dr.current_url)\n        elem = dr.find_element_by_xpath('//*[@id=\"phone-number\"]')\n        elem.click()\n        elem.send_keys(\"123456\")\n        #dr.save_screenshot(\"s.png\"\n\n\ndef test_self_driver():\n    d = adbutils.adb.device()\n    package_name = \"com.xueqiu.android\"\n    # package_name = \"io.appium.android.apis\"\n    d.forward(\"tcp:7912\", \"tcp:7912\")\n    ret = requests.get(f\"http://localhost:7912/proc/{package_name}/webview\").json()\n    for data in ret:\n        pprint(data)\n        lport = d.forward_port(\"localabstract:\"+data[\"socketPath\"])\n        wd = WebviewDriver(f\"http://localhost:{lport}\")\n        tabs = wd.get_active_tab_list()\n        pprint(tabs)\n        for tab in tabs:\n            print(tab.current_url())\n            tab.click_by_xpath('//*[@id=\"phone-number\"]')\n            tab.clear_text_by_xpath('//*[@id=\"phone-number\"]')\n            tab.send_keys(\"123456789\")\n        break\n\n\ndef runtest():\n    import uiautomator2 as u2\n\n    d = u2.connect_usb()\n    pprint(d.request_agent(\"/webviews\").json())\n    port = d.adb_device.forward_port(\"localabstract:chrome_devtools_remote\")\n    wd = WebviewDriver(f\"http://localhost:{port}\")\n    tabs = wd.get_active_tab_list()\n    pprint(tabs)\n\n\ndef main():\n    import argparse\n    parser = argparse.ArgumentParser()\n    parser.add_argument(\"-t\", \"--test\", action=\"store_true\", help=\"run test_self_driver\")\n    args = parser.parse_args()\n\n    # WebviewDriver()\n    import uiautomator2 as u2\n\n    d = u2.connect_usb()\n    assert d.adb_device, \"must connect with usb\"\n    for socket_path in d.request_agent(\"/webviews\").json():\n        port = d.adb_device.forward_port(\"localabstract:\"+socket_path)\n        data = requests.get(f\"http://localhost:{port}/json/version\").json()\n        import pprint\n        pprint.pprint(data)\n    \n\nif __name__ == \"__main__\":\n    main()\n    # if args.test:\n    #     print(\"---- test ----\")\n    #     test_self_driver()\n    # else:\n    #     chromedriver()\n"
  },
  {
    "path": "_archived/widget.py",
    "content": "# coding: utf-8\n#\n#  DEPRECATED\n#\n#  This file is deprecated and will be removed in the future.\nimport logging\nimport re\nimport time\nfrom collections import defaultdict, namedtuple\nfrom functools import partial\nfrom pprint import pprint\nfrom typing import Union\n\nimport requests\nfrom lxml import etree\n\nimport uiautomator2 as u2\nfrom uiautomator2.image import compare_ssim, draw_point, imread\n\nlogger = logging.getLogger(__name__)\n\n\ndef xml2nodes(xml_content: Union[str, bytes]):\n    if isinstance(xml_content, str):\n        xml_content = xml_content.encode(\"utf-8\")\n\n    root = etree.fromstring(xml_content)\n    nodes = []\n    for _, n in etree.iterwalk(root):\n        attrib = dict(n.attrib)\n        if \"bounds\" in attrib:\n            bounds = re.findall(r\"(\\d+)\", attrib.pop(\"bounds\"))\n            if len(bounds) != 4:\n                continue\n            lx, ly, rx, ry = map(int, bounds)\n            attrib['size'] = (rx - lx, ry - ly)\n        attrib.pop(\"index\", None)\n\n        ok = False\n        for attrname in (\"text\", \"resource-id\", \"content-desc\"):\n            if attrname in attrib:\n                ok = True\n                break\n        if ok:\n            items = []\n            for k, v in sorted(attrib.items()):\n                items.append(k + \":\" + str(v))\n            nodes.append('|'.join(items))\n    return nodes\n\n\ndef hierarchy_sim(xml1: str, xml2: str):\n    ns1 = xml2nodes(xml1)\n    ns2 = xml2nodes(xml2)\n\n    from collections import Counter\n    c1 = Counter(ns1)\n    c2 = Counter(ns2)\n\n    same_count = sum(\n        [min(c1[k], c2[k]) for k in set(c1.keys()).intersection(c2.keys())])\n    logger.debug(\"Same count: %d ns1: %d ns2: %d\", same_count, len(ns1), len(ns2))\n    return same_count / (len(ns1) + len(ns2)) * 2\n\n\ndef read_file_content(filename: str) -> bytes:\n    with open(filename, \"rb\") as f:\n        return f.read()\n\n\ndef safe_xmlstr(s):\n    return s.replace(\"$\", \"-\")\n\n\ndef frozendict(d: dict):\n    items = []\n    for k, v in sorted(d.items()):\n        items.append(k + \":\" + str(v))\n    return '|'.join(items)\n\n\nCompareResult = namedtuple(\"CompareResult\", [\"score\", \"detail\"])\nPoint = namedtuple(\"Point\", ['x', 'y'])\n\n\nclass Widget(object):\n    __domains = {\n        \"lo\": \"http://localhost:17310\",\n    }\n\n    def __init__(self, d: \"u2.Device\"):\n        self._d = d\n        self._widgets = {}\n        self._compare_results = {}\n\n        self.popups = []\n\n    @property\n    def wait_timeout(self):\n        return self._d.settings['wait_timeout']\n\n    def _get_widget(self, id: str):\n        if id in self._widgets:\n            return self._widgets[id]\n        widget_url = self._id2url(id)\n        r = requests.get(widget_url, timeout=3)\n        data = r.json()\n        self._widgets[id] = data\n        return data\n\n    def _id2url(self, id: str):\n        fields = re.sub(\"#.*\", \"\", id).split(\n            \"/\")  # remove chars after # and split host and id\n        assert len(fields) <= 2\n        if len(fields) == 1:\n            return f\"http://localhost:17310/api/v1/widgets/{id}\"\n\n        host = self.__domains.get(fields[0])\n        id = fields[1]  # ignore the third part\n        if not re.match(\"^https?://\", host):\n            host = \"http://\" + host\n        return f\"{host}/api/v1/widgets/{id}\"\n\n    def _eq(self, precision: float, a, b):\n        return abs(a - b) < precision\n\n    def _percent_equal(self, precision: float, a, b, asize, bsize):\n        return abs(a / min(asize) - b / min(bsize)) < precision\n\n    def _bounds2rect(self, bounds: str):\n        \"\"\"\n        Returns:\n            tuple: (lx, ly, width, height)\n        \"\"\"\n        if not bounds:\n            return 0, 0, 0, 0\n        lx, ly, rx, ry = map(int, re.findall(r\"\\d+\", bounds))\n        return (lx, ly, rx - lx, ry - ly)\n\n    def _compare_node(self, node_a, node_b, size_a, size_b) -> float:\n        \"\"\"\n        Args:\n            node_a, node_b: etree.Element\n            size_a, size_b: tuple size\n        \n        Returns:\n            CompareResult\n        \"\"\"\n        result_key = (node_a, node_b)\n        if result_key in self._compare_results:\n            return self._compare_results[result_key]\n\n        scores = defaultdict(dict)\n\n        # max 1\n        if node_a.tag == node_b.tag:\n            scores['class'] = 1\n\n        # max 3\n        for key in ('text', 'resource-id', 'content-desc'):\n            if node_a.attrib.get(key) == node_b.attrib.get(key):\n                scores[key] = 1 if node_a.attrib.get(key) else 0.1\n\n        # bounds = node_a.attrib.get(\"bounds\")\n        # pprint(list(map(int, re.findall(r\"\\d+\", bounds))))\n        ax, ay, aw, ah = self._bounds2rect(node_a.attrib.get(\"bounds\"))\n        bx, by, bw, bh = self._bounds2rect(node_b.attrib.get(\"bounds\"))\n\n        # max 2\n        peq = partial(self._percent_equal, 1 / 20, asize=size_a, bsize=size_b)\n        if peq(ax, bx) and peq(ay, by):\n            scores['left_top'] = 1\n        if peq(aw, bw) and peq(ah, bh):\n            scores['size'] = 1\n\n        score = round(sum(scores.values()), 1)\n        result = self._compare_results[result_key] = CompareResult(\n            score, scores)\n        return result\n\n    def node2string(self, node: etree.Element):\n        return node.tag + \":\" + '|'.join([\n            node.attrib.get(key, \"\")\n            for key in [\"text\", \"resource-id\", \"content-desc\"]\n        ])\n\n    def hybird_compare_node(self, node_a, node_b, size_a, size_b):\n        \"\"\"\n        Returns:\n            (scores, results)\n        \n        Return example:\n            【3.0, 3.2], [CompareResult(score=3.0), CompareResult(score=3.2)]\n        \"\"\"\n        cmp_node = partial(self._compare_node, size_a=size_a, size_b=size_b)\n\n        results = []\n\n        results.append(cmp_node(node_a, node_b))\n        results.append(cmp_node(node_a.getparent(), node_b.getparent()))\n\n        a_children = node_a.getparent().getchildren()\n        b_children = node_b.getparent().getchildren()\n        if len(a_children) != len(b_children):\n            return results\n\n        children_result = []\n        a_children.remove(node_a)\n        b_children.remove(node_b)\n        for i in range(len(a_children)):\n            children_result.append(cmp_node(a_children[i], b_children[i]))\n        results.append(children_result)\n        return results\n\n    def _hybird_result_to_score(self, obj: Union[list, CompareResult]):\n        \"\"\"\n        Convert hybird_compare_node returns to score\n        \"\"\"\n        if isinstance(obj, CompareResult):\n            return obj.score\n        ret = []\n        for item in obj:\n            ret.append(self._hybird_result_to_score(item))\n        return ret\n\n    def replace_etree_node_to_class(self, root: etree.ElementTree):\n        for node in root.xpath(\"//node\"):\n            node.tag = safe_xmlstr(node.attrib.pop(\"class\", \"\") or \"node\")\n        return root\n\n    def compare_hierarchy(self, node, root, node_wsize, root_wsize):\n        results = {}\n        for node2 in root.xpath(\"/hierarchy//*\"):\n            result = self.hybird_compare_node(node, node2, node_wsize, root_wsize)\n            results[node2] = result  #score\n        return results\n\n    def etree_fromstring(self, s: str):\n        root = etree.fromstring(s.encode('utf-8'))\n        return self.replace_etree_node_to_class(root)\n\n    def node_center_point(self, node) -> Point:\n        lx, ly, rx, ry = map(int, re.findall(r\"\\d+\",\n                                             node.attrib.get(\"bounds\")))\n        return Point((lx + rx) // 2, (ly + ry) // 2)\n\n    def match(self, widget: dict, hierarchy=None, window_size: tuple = None):\n        \"\"\"\n        Args:\n            widget: widget id\n            hierarchy (optional): current page hierarchy\n            window_size (tuple): width and height\n\n        Returns:\n            None or MatchResult(point, score, detail, xpath, node, next_result)\n        \"\"\"\n        window_size = window_size or self._d.window_size()\n        hierarchy = hierarchy or self._d.dump_hierarchy()\n        w = widget.copy()\n\n        widget_root = self.etree_fromstring(w['hierarchy'])\n        widget_node = widget_root.xpath(w['xpath'])[0]\n\n        # 节点打分\n        target_root = self.etree_fromstring(hierarchy)\n        results = self.compare_hierarchy(widget_node, target_root, w['window_size'], window_size) # yapf: disable\n\n        # score结构调整\n        scores = {}\n        for node, result in results.items():\n            scores[node] = self._hybird_result_to_score(result) # score eg: [3.2, 2.2, [1.0, 1.2]]\n\n        # 打分排序\n        nodes = list(scores.keys())\n        nodes.sort(key=lambda n: scores[n], reverse=True)\n        possible_nodes = nodes[:10]\n        \n        # compare image\n        # screenshot = self._d.screenshot()\n        # for node in possible_nodes:\n        #     bounds = node.attrib.get(\"bounds\")\n        #     lx, ly, rx, ry = bounds = list(map(int, re.findall(r\"\\d+\", bounds)))\n        #     w, h = rx - lx, ry - ly\n        #     crop_image = screenshot.crop(bounds)\n        #     template = imread(w['target_image']['url'])\n        #     try:\n        #         score = compare_ssim(template, crop_image)\n        #         scores[node][0] += score\n        #     except ValueError:\n        #         pass\n        # nodes.sort(key=lambda n: scores[n], reverse=True)\n\n        first, second = nodes[:2]\n\n        MatchResult = namedtuple(\n            \"MatchResult\",\n            [\"point\", \"score\", \"detail\", \"xpath\", \"node\", \"next_result\"])\n\n        def get_result(node, next_result=None):\n            point = self.node_center_point(node)\n            xpath = node.getroottree().getpath(node)\n            return MatchResult(point, scores[node], results[node], xpath,\n                               node, next_result)\n\n        return get_result(first, get_result(second))\n\n    def exists(self, id: str) -> bool:\n        pass\n\n    def update_widget(self, id, hierarchy, xpath):\n        url = self._id2url(id)\n        r = requests.put(url, json={\"hierarchy\": hierarchy, \"xpath\": xpath})\n        print(r.json())\n\n    def wait(self, id: str, timeout=None):\n        \"\"\"\n        Args:\n            timeout (float): seconds to wait\n\n        Returns:\n            None or Result\n        \"\"\"\n        timeout = timeout or self.wait_timeout\n        widget = self._get_widget(id) # 获取节点信息\n\n        begin_time = time.time()\n        deadline = time.time() + timeout\n\n        while time.time() < deadline:\n            hierarchy = self._d.dump_hierarchy()\n            hsim = hierarchy_sim(hierarchy, widget['hierarchy'])\n\n            app = self._d.app_current()\n            is_same_activity = widget['activity'] == app['activity']\n            if not is_same_activity:\n                print(\"activity different:\", \"got\", app['activity'], 'expect', widget['activity'])\n            print(\"hierarchy: %.1f%%\" % hsim)\n            print(\"----------------------\")\n\n            window_size = self._d.window_size()\n\n            page_ok = False\n            if is_same_activity:\n                if hsim > 0.7:\n                    page_ok = True\n                if time.time() - begin_time > 10.0 and hsim > 0.6:\n                    page_ok = True\n\n            if page_ok:\n                result = self.match(widget, hierarchy, window_size)\n                if result.score[0] < 2:\n                    time.sleep(0.5)\n                    continue\n\n                if hsim < 0.8:\n                    self.update_widget(id, hierarchy, result.xpath)\n                return result\n            time.sleep(1.0)\n\n    def click(self, id: str, debug: bool = False, timeout=10):\n        print(\"Click\", id)\n\n        result = self.wait(id, timeout=timeout)\n        if result is None:\n            raise RuntimeError(\"target not found\")\n\n        x, y = result.point\n        if debug:\n            show_click_position(self._d, Point(x, y))\n        self._d.click(x, y)\n        # return\n\n        # while True:\n        #     hierarchy = self._d.dump_hierarchy()\n        #     hsim = hierarchy_sim(hierarchy, widget['hierarchy'])\n\n        #     app = self._d.app_current()\n        #     is_same_activity = widget['activity'] == app['activity']\n\n        #     print(\"activity same:\", is_same_activity)\n        #     print(\"hierarchy:\", hsim)\n\n        #     window_size = self._d.window_size()\n\n        #     if is_same_activity and hsim > 0.8:\n        #         result = self.match(widget, hierarchy, window_size)\n        #         pprint(result.score)\n        #         pprint(result.second.score)\n        #         x, y = result.point\n        #         self._d.click(x, y)\n        #         return\n        #     time.sleep(0.1)\n        # return\n\n\ndef show_click_position(d: u2.Device, point: Point):\n    # # pprint(result.widget)\n    # # pprint(dict(result.node.attrib))\n    im = draw_point(d.screenshot(), point.x, point.y)\n    im.show()\n\n\ndef main():\n    d = u2.connect(\"30.10.93.26\")\n\n    # d.widget.click(\"00013#推荐歌单第一首\")\n\n    d.widget.exists(\"lo/00019#播放全部\")\n    return\n\n    d.widget.click(\"00019#播放全部\")\n    # d.widget.click(\"00018#播放暂停\")\n    d.widget.click(\"00018#播放暂停\")\n    d.widget.click(\"00021#转到上一层级\")\n    return\n\n    d.widget.click(\"每日推荐\")\n    widget_id = \"00009#上新\"\n    widget_id = \"00011#每日推荐\"\n    widget_id = \"00014#立减20\"\n    result = d.widget.match(widget_id)\n    # e = Widget(d)\n    # result = e.match(\"00003\")\n    # print(result)\n    # # e.match(\"00002\")\n    # # result = e.match(\"00007\")\n\n    wsize = d.window_size()\n    from lxml import etree\n\n    result = d.widget.match(widget_id)\n    pprint(result.node.attrib)\n    pprint(result.score)\n    pprint(result.detail)\n\n    show_click_position(d, result.point)\n    return\n\n    root = etree.parse(\n        '/Users/shengxiang/Projects/weditor/widgets/00010/hierarchy.xml')\n    nodes = root.xpath('/hierarchy/node/node/node/node')\n    a, b = nodes[0], nodes[1]\n    result = d.widget.hybird_compare_node(a, b, wsize, wsize)\n    pprint(result)\n    score = d.widget._hybird_result_to_score(result)\n    pprint(score)\n    return\n\n    score = d.widget._compare_node(a, b, wsize, wsize)\n    print(score)\n\n    a, b = nodes[0].getparent(), nodes[1].getparent()\n    score = d.widget._compare_node(a, b, wsize, wsize)\n    pprint(score)\n\n    return\n\n    print(\"score:\", result.score)\n    x, y = result.point\n    # # pprint(result.widget)\n    # # pprint(dict(result.node.attrib))\n    pprint(result.detail)\n    im = draw_point(d.screenshot(), x, y)\n    im.show()\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "demo_tests/conftest.py",
    "content": "# coding: utf-8\n# author: codeskyblue\n\nimport pytest\n\nimport uiautomator2 as u2\n\n\n@pytest.fixture(scope=\"function\")\ndef d():\n    _d = u2.connect_usb()\n    _d.press(\"home\")\n    yield _d\n    \n\n@pytest.fixture(scope=\"function\")\ndef app(d: u2.Device):\n    d.app_start(\"com.example.u2testdemo\", stop=True)\n    d(text=\"Addition\").wait()\n    yield d"
  },
  {
    "path": "demo_tests/test_app.py",
    "content": "# coding: utf-8\n# author: codeskyblue\n\nimport pytest\n\nimport uiautomator2 as u2\n\nPACKAGE = \"com.example.u2testdemo\"\n\n\ndef test_wait_activity(d: u2.Device):\n    # assert app.wait_activity('.MainActivity', timeout=10)\n    \n    d.app_start(PACKAGE, activity=\".AdditionActivity\", wait=True)\n    assert d.wait_activity('.AdditionActivity', timeout=3)\n    assert not d.wait_activity('.NotExistActivity', timeout=1)\n\n\ndef test_app_wait(app: u2.Device):\n    assert app.app_wait(PACKAGE, front=True)\n\n\ndef test_app_start_stop(d: u2.Device):\n    assert PACKAGE in d.app_list()\n    d.app_stop(PACKAGE)\n    assert PACKAGE not in d.app_list_running()\n    d.app_start(PACKAGE, wait=True)\n    assert PACKAGE in d.app_list_running()\n    \n\ndef test_app_clear(d: u2.Device):\n    d.app_clear(PACKAGE)\n    # d.app_stop_all()\n\n\ndef test_app_info(d: u2.Device):\n    d.app_info(PACKAGE)\n    with pytest.raises(u2.AppNotFoundError):\n        d.app_info(\"not.exist.package\")\n\n\ndef test_auto_grant_permissions(d: u2.Device):\n    d.app_auto_grant_permissions(PACKAGE)\n\n\ndef test_session(d: u2.Device):\n    app = d.session(PACKAGE)\n    assert app.running() is True\n    assert app.pid > 0\n    old_pid = app.pid\n    \n    app.restart()\n    assert old_pid != app.pid\n    \n    with app:\n        app(text=\"Addition\").info\n    "
  },
  {
    "path": "demo_tests/test_core.py",
    "content": "# coding: utf-8\n# author: codeskyblue\n\nfrom typing import Optional\n\nimport uiautomator2 as u2\n\n\ndef get_app_process_pid(d: u2.Device) -> Optional[int]:\n    for line in d.shell(\"ps -u shell\").output.splitlines():\n        fields = line.split()\n        if fields[-1] == 'app_process':\n            pid = fields[1]\n            return int(pid)\n    return None\n\n\ndef kill_app_process(d: u2.Device) -> bool:\n    pid = get_app_process_pid(d)\n    if not pid:\n        return False\n    d.shell(f\"kill {pid}\")\n    return True\n\n\ndef test_uiautomator_keeper(d: u2.Device):\n    kill_app_process(d)\n    d.sleep(.2)\n    assert get_app_process_pid(d) is None\n    d.shell('rm /data/local/tmp/u2.jar')\n    \n    d.start_uiautomator()\n    assert get_app_process_pid(d) > 0\n    \n    d.stop_uiautomator()\n    assert get_app_process_pid(d) is None\n\n\ndef test_debug(d: u2.Device):\n    d.debug = True\n    d.info\n    "
  },
  {
    "path": "demo_tests/test_device.py",
    "content": "# coding: utf-8\n# author: codeskyblue\n\nimport random\nfrom pathlib import Path\n\nimport pytest\nfrom PIL import Image\n\nimport uiautomator2 as u2\n\n\ndef test_info(d: u2.Device):\n    d.info\n    d.device_info\n    d.wlan_ip\n    assert isinstance(d.serial, str)\n    \n    w, h = d.window_size()\n    assert w > 0 and h > 0\n    \n\ndef test_dump_hierarchy(d: u2.Device):\n    assert d.dump_hierarchy().startswith(\"<?xml\")\n    assert d.dump_hierarchy(compressed=True, pretty=True).startswith(\"<?xml\")\n    \n\ndef test_screenshot(d: u2.Device, tmp_path: Path):\n    im = d.screenshot()\n    assert isinstance(im, Image.Image)\n    \n    d.screenshot(tmp_path / \"screenshot.png\")\n    assert (tmp_path / \"screenshot.png\").exists()\n\n\ndef test_settings(d: u2.Device):\n    d.implicitly_wait(10)\n\n\ndef test_click(app: u2.Device):\n    app.click(1, 1)\n    app.long_click(1, 1)\n    app.click(0.5, 0.5)\n    app.double_click(1, 1)\n\n\ndef test_swipe_drag(app: u2.Device):\n    app.swipe(1, 1, 2, 2, steps=20)\n    app.swipe(1, 1, 2, 2, duration=.1)\n    app.swipe(1, 1, 2, 2)\n    with pytest.warns(UserWarning):\n        app.swipe(1, 1, 2, 2, 0.1, 20)\n    \n    app.swipe_points([(1, 1), (2, 2)], duration=0.1)\n    app.drag(1, 1, 2, 2, duration=0.1)\n\n\n@pytest.mark.parametrize(\"direction\", [\"up\", \"down\", \"left\", \"right\"])\ndef test_swipe_ext(d: u2.Device, direction: str):\n    d.swipe_ext(direction)\n\n\ndef test_swipe_ext_inside_box(app: u2.Device):\n    bounds = app.xpath('@android:id/content').get().bounds\n    app.swipe_ext(\"up\", box=bounds)\n\n\ndef test_press(d: u2.Device):\n    d.press(\"volume_down\")\n    # press home keycode\n    d.press(3)\n    \n    d.long_press(\"volume_down\")\n    # long volume_down keycode\n    d.long_press(25)\n    \n    d.keyevent(\"volume_down\")\n\n\ndef test_screen(d: u2.Device):\n    # d.screen_off()\n    d.screen_on()\n\n\ndef test_orientation(d: u2.Device):\n    with pytest.raises(ValueError):\n        d.orientation = 'unknown'\n        \n    d.orientation = 'n'\n    assert d.orientation == 'natural'\n    d.freeze_rotation(True)\n    d.freeze_rotation(False)\n    \n\ndef test_traversed_text(d: u2.Device):\n    d.last_traversed_text\n    d.clear_traversed_text()\n\n\ndef test_open(d: u2.Device):\n    d.open_notification()\n    d.open_quick_settings()\n    d.open_url(\"https://www.baidu.com\")\n\n\ndef test_toast(app: u2.Device):\n    app.clear_toast()\n    assert app.last_toast is None\n    \n    app(text='Toast').click()\n    app(text='Show Toast').click()\n    app.sleep(.2)\n    assert app.last_toast == \"Button Clicked!\"\n    \n    app.clear_toast()\n    assert app.last_toast is None\n\n\ndef test_clipboard(d: u2.Device):\n    d.set_input_ime()\n    text = str(random.randint(0, 1000))\n    d.clipboard = f'n{text}'\n    assert d.clipboard == f'n{text}'\n\n\ndef test_push_pull(d: u2.Device, tmp_path: Path):\n    src_file = tmp_path / \"test_push.txt\"\n    src_file.write_text(\"12345\")\n    d.push(src_file, \"/data/local/tmp/test_push.txt\")\n    \n    dst_file = tmp_path / \"test_pull.txt\"\n    d.pull(\"/data/local/tmp/test_push.txt\", dst_file)\n    assert dst_file.read_text() == \"12345\""
  },
  {
    "path": "demo_tests/test_input.py",
    "content": "# coding: utf-8\n# author: codeskyblue\n\nimport pytest\n\nimport uiautomator2 as u2\n\n\ndef test_set_ime(d: u2.Device):\n    d.set_input_ime(True)\n    d.set_input_ime(False)\n\n\ndef test_send_keys(app: u2.Device):\n    app(text=\"Addition\").click()\n    num1 = app(className=\"android.widget.EditText\", instance=0)\n    num2 = app(className=\"android.widget.EditText\", instance=1)\n    result = app(className=\"android.widget.EditText\", instance=2)\n    \n    num1.set_text(\"5\")\n    assert num1.get_text() == \"5\"\n    num1.clear_text()\n    assert num1.get_text() == ''\n    num1.set_text('1')\n    \n    num2.click()\n    \n    for chars in ('1', '123abcDEF +-*/_', '你好，世界!'):\n        app.send_keys(chars, clear=True)\n        assert num2.get_text() == chars\n    \n    app.clear_text()\n    app.send_keys('2')\n    app(text=\"Add\").click()\n    result = app(className=\"android.widget.EditText\", instance=2).get_text()\n    assert result == \"3\"\n\n\ndef test_send_action(): # TODO\n    pass"
  },
  {
    "path": "demo_tests/test_selector.py",
    "content": "# coding: utf-8\n# author: codeskyblue\n\nimport time\n\nimport pytest\n\nimport uiautomator2 as u2\nfrom uiautomator2 import Selector\n\n\ndef test_selector_magic():\n    s = Selector(text='123').child(text='456').sibling(text='789').clone()\n    assert s['text'] == '123'\n    del s['text']\n    assert 'text' not in s\n    s.update_instance(0)\n    \n\ndef test_exists(app: u2.Device):\n    assert app(text='Addition').exists\n    assert app(text='Addition').exists(timeout=.1)\n    assert not app(text='should-not-exists').exists\n    assert not app(text='should-not-exists').exists(timeout=.1)\n    \n\ndef test_selector_info(app: u2.Device):\n    _info = app(text=\"Addition\").info\n    assert _info[\"text\"] == \"Addition\"\n    \n\n@pytest.mark.skip(reason=\"not stable\")\ndef test_child_by(app: u2.Device):\n    app(text=\"Addition\").click()\n    app(text='Add').wait()\n    time.sleep(.5)\n    # childByText, childByInstance and childByDescription run query when called\n    app(resourceId='android:id/content').child_by_text(\"Add\")\n    app(resourceId='android:id/content').child_by_instance(0)\n    with pytest.raises(u2.UiObjectNotFoundError):\n        app(resourceId='android:id/content').child_by_description(\"should-not-exists\")\n    \n    # only run query after call UiObject method\n    assert app(resourceId='android:id/content').child_selector(text=\"Add\").exists\n\n\ndef test_screenshot(app: u2.Device):\n    lx, ly, rx, ry = app(text=\"Addition\").bounds()\n    image = app(text=\"Addition\").screenshot()\n    assert image.size == (rx - lx, ry - ly)\n\n\ndef test_center(app: u2.Device):\n    x, y = app(text=\"Addition\").center()\n    assert x > 0 and y > 0\n    \n\ndef test_click_exists(app: u2.Device):\n    assert app(text=\"Addition\").click_exists()\n    app(text='Addition').wait_gone()\n    assert not app(text=\"should-not-exists\").click_exists()\n\n\n@pytest.mark.parametrize(\"direction\", [\"up\", \"down\", \"left\", \"right\"])\ndef test_swipe(app: u2.Device, direction: str):\n    app(resourceId=\"android:id/content\").swipe(direction)\n\n\ndef test_pinch_gesture(app: u2.Device):\n    app(text='Pinch').click()\n    app(description='pinch image').wait()\n    scale_text = app.xpath('Scale%').get_text()\n    assert scale_text.endswith('1.00')\n    \n    app(description='pinch image').pinch_in(80)\n    scale_text = app.xpath('Scale%').get_text()\n    assert scale_text.endswith('0.50')\n    \n    app(description='pinch image').pinch_out()\n    scale_text = app.xpath('Scale%').get_text()\n    assert scale_text.endswith('3.00')\n    \n    app().gesture((0.1, 0.5), (0.9, 0.5), (0.5, 0.5), (0.5, 0.5), steps=20)\n    scale_text = app.xpath('Scale%').get_text()\n    assert scale_text.endswith('0.50')\n\n\n# TODO\n# long_click\n# drag_to\n# swipe\n# guesture"
  },
  {
    "path": "demo_tests/test_watcher.py",
    "content": "# coding: utf-8\n# author: codeskyblue\n\n# TODO"
  },
  {
    "path": "docs/2to3.md",
    "content": "# 2.x到3.x升级说明\n\n## 变更内容简介\n\n- 移除atx-agent，常驻服务移除，改为运行时启动手机内的uiautomator服务\n- 直接通过atx-agent地址连接的方法不再支持，connect函数目前只支持本地的USB设备或adb connect状态的设备\n- 环境变量 ANDROID_DEVICE_IP 不再支持，如果需要使用环境变量传递序列号可以使用 ANDROID_SERIAL\n- 版本管理从pbr改为poetry，同时降低依赖库的数量\n- Python依赖调整为最低3.8\n- 增加更多的单测\n- 日志使用标准库logging, 默认不输出任何内容，除非手动开启\n- minicap,minitouch不再默认安装\n- ~~atx-agent[armeabi]直接打包到lib里面去，避免外网下载依赖~~\n\n\n## New\n- Add function enable_pretty_logging\n- Add class AdbShellError, HierarchyEmptyError, HTTPError\n- Add d.xpath.get_page_source() -> PageSource\n\n## Breaking changes (移除的功能)\n\n### Lib removes\n- Remove logzero\n- Remove filelocks\n- Remove progress\n- Remove packaging\n- Remove Pillow\n\n### Module removes\n- Remove module uiautomator2.ext.xpath\n\n### Class removes\n- Remove class AdbUI\n- Remove class GatewayError\n- Remove class ServerError, UiautomatorQuitError, RequestError, UiaError, JsonRpcError, NullObjectExceptionError, NullPointerExceptionError, StaleObjectExceptionError\n\n### Property removes\n- Remove property u2.logger\n- Remove property u2.xpath.XPath.logger\n- Remove property d.watcher.debug\n- Remove property: address, 原来是用于获取atx-agent的URL地址\n- Remove property: alive, 原来用于检测atx-agent的存活状态\n- Remove property: uiautomator, 原来用法是d.uiautomator.stop()\n- Remove property: widget, 原来也不怎么用\n- Remove property: http, 原来是这样用的d.http.get(\"/device_info\")\n- Remove d.settings[\"xpath_debug\"]\n\n### Function removes\n- Remove function current_app, use app_current instead\n- Remove function d.xpath.apply_watch_from_yaml\n- Remove function healcheck, 原来是未来恢复uiautomator服务用的\n- Remove function service(name: str) -> Service, 原本是用于做atx-agent的服务管理\n- Remove function app_icon() -> Image, 该函数依赖atx-agent\n- Remove function connect_wifi() -> Device, 该函数依赖atx-agent\n- Remove function connect_adb_wifi(str) -> Device, 直接用connect就行了\n- Remove function set_new_command_timeout(timeout: int), 用不着了\n- Remove function open_identify(), 打开一个比较明显的界面，这个函数出了点毛病，先下掉了\n- Remove function toast.show(text, duration), 用的不多而且稳定性不好\n\nXPath (d.xpath) methods\n- remove dump_hierarchy\n- remove get_last_hierarchy\n- remove add_event_listener\n- remove send_click, send_longclick, send_swipe, send_text, take_screenshot\n- remove when, run_watchers, watch_background, watch_stop, watch_clear, sleep_watch\n- remove position method, usage like d.xpath(...).position(0.2, 0.2)\n\nInputMethod\n- deprecated wait_fastinput_ime\n- deprecated set_fastinput_ime use set_input_ime instead\n\n### Command remove\n- Remove \"uiautomator2 healthcheck\"\n- Remove \"uiautomator2 identify\"\n\n## Function changes\n### connect_usb\n- 2.x connect_usb(serial, init: bool)\n- 3.x connect_usb(serial)\n\n### shell\n- 2.x shell(cmdargs, stream: bool, timeout)\n- 3.x shell(cmdargs, timeout)\n\n### push\n- 2.x push(src, dst, mode, show_process: bool)\n- 3.x push(src, dst, mode)\n\n### xpath\n- 2.x `d.xpath(\"...\").wait() -> XMLElement|None`\n- 3.x `d.xpath(\"...\").wait() -> bool`\n\n### reset_uiautomator\n- 2.x reset_uiautomator(self, reason=\"unknown\", depth=0)\n- 3.x reset_uiautomator()\n\n### app_info\n2.x Response\n\n```json\n{\n    \"mainActivity\": \"com.github.uiautomator.MainActivity\",\n    \"label\": \"ATX\",\n    \"versionName\": \"1.1.7\",\n    \"versionCode\": 1001007,\n    \"size\":1760809\n}\n```\n\n3.x Response\n\n```json\n{\n    \"versionName\": \"1.1.7\",\n    \"versionCode\": 1001007,\n}\n```\n\n### session\n- 2.x sess = d.session(\"com.netease.cloudmusic\", attach=True, strict=True)\n- 3.x sess = d.session(\"com.netease.cloudmusic\", attach=True)\n\nIt seems the strict is useless, so I delete it.\n\n### push\n- 2.x push(src, dst, mode, show_process:bool=False)\n- 3.x push(src, dst, mode)\n\n### device_info\nprint(d.device_info)\n\n2.x prints\n\n```\n{'udid': '08a3d291-26:17:84:b6:cb:a0-DT1901A',\n 'version': '10',\n 'serial': '08a3d291',\n 'brand': 'SMARTISAN',\n 'model': 'DT1901A',\n 'hwaddr': '26:17:84:b6:cb:a0',\n 'sdk': 29,\n 'agentVersion': '0.10.0',\n 'display': {'width': 1080, 'height': 2340},\n 'battery': {'acPowered': False,\n  'usbPowered': True,\n  'wirelessPowered': False,\n  'status': 5,\n  'health': 2,\n  'present': True,\n  'level': 100,\n  'scale': 100,\n  'voltage': 4356,\n  'temperature': 292,\n  'technology': 'Li-poly'},\n 'memory': {'total': 7665272, 'around': '7 GB'},\n 'cpu': {'cores': 8, 'hardware': 'Qualcomm Technologies, Inc SM8150'},\n 'arch': '',\n 'owner': None,\n 'presenceChangedAt': '0001-01-01T00:00:00Z',\n 'usingBeganAt': '0001-01-01T00:00:00Z',\n 'product': None,\n 'provider': None}\n ```\n\n 3.x prints\n\n ```\n {'serial': 'VVY0223208008426',\n 'sdk': 31,\n 'brand': 'HUAWEI',\n 'model': 'JAD-AL80',\n 'arch': 'arm64-v8a',\n 'version': 12}\n ```\n\n### app_current\n - 2.x raise `OSError` if couldn't get focused app\n - 3.x raise `DeviceError` if couldn't get focused app\n\n### current_ime\n- 2.x return (ime_method_name, bool), e.g. (\"com.github.uiautomator/.FastInputIME\", True)\n- 3.x return ime_method_name, e.g. \"com.github.uiautomator/.FastInputIME\"\n\n### toast\n- 2.x d.toast.get_message(5.0, default=\"\")\n- 3.x d.last_toast (property)\n\n- 2.x d.toast.reset()\n- 3.x d.clear_toast()"
  },
  {
    "path": "docs/Makefile",
    "content": "# Minimal makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line, and also\n# from the environment for the first two.\nSPHINXOPTS    ?=\nSPHINXBUILD   ?= sphinx-build\nSOURCEDIR     = .\nBUILDDIR      = _build\n\n# Put it first so that \"make\" without argument is like \"make help\".\nhelp:\n\t@$(SPHINXBUILD) -M help \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n\nhtml:\n\tsphinx-apidoc -o . ../\n\t$(SPHINXBUILD) -M $@ \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(0)\n\npublish: html\n\t@oss_upload.py --directory --prefix uiautomator2-docs _build/html\n\t@echo \"URL: http://tmallwireless-ycombinator.cn-hangzhou.oss-cdn.aliyun-inc.com/uiautomator2-docs/index.html\"\n\n.PHONY: help Makefile\n\n# Catch-all target: route all unknown targets to Sphinx using the new\n# \"make mode\" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).\n%: Makefile\n\t@$(SPHINXBUILD) -M $@ \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n"
  },
  {
    "path": "docs/conf.py",
    "content": "# Configuration file for the Sphinx documentation builder.\n#\n# This file only contains a selection of the most common options. For a full\n# list see the documentation:\n# https://www.sphinx-doc.org/en/master/usage/configuration.html\n\n# -- Path setup --------------------------------------------------------------\n\n# If extensions (or modules to document with autodoc) are in another directory,\n# add these directories to sys.path here. If the directory is relative to the\n# documentation root, use os.path.abspath to make it absolute, like shown here.\n#\n#import os\n#import sys\n#sys.path.insert(0, os.path.abspath('../uiautomator2'))\n\nimport uiautomator2\n\n# -- Project information -----------------------------------------------------\n\nmaster_doc = 'index'\n\nproject = 'uiautomator2'\ncopyright = '2020, codeskyblue'\nauthor = 'codeskyblue'\n\n\n# -- General configuration ---------------------------------------------------\n\n# Add any Sphinx extension module names here, as strings. They can be\n# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom\n# ones.\nextensions = [\n    'sphinx.ext.autodoc',\n    'sphinx.ext.autosummary',\n    'sphinx.ext.coverage',\n    'sphinx.ext.viewcode',\n]\n\n# Add any paths that contain templates here, relative to this directory.\ntemplates_path = ['_templates']\n\n# List of patterns, relative to source directory, that match files and\n# directories to ignore when looking for source files.\n# This pattern also affects html_static_path and html_extra_path.\nexclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']\n\n\n# -- Options for HTML output -------------------------------------------------\n\n# The theme to use for HTML and HTML Help pages.  See the documentation for\n# a list of builtin themes.\n#\nhtml_theme = 'alabaster'\n\n# Add any paths that contain custom static files (such as style sheets) here,\n# relative to this directory. They are copied after the builtin static files,\n# so a file named \"default.css\" will overwrite the builtin \"default.css\".\nhtml_static_path = ['_static']\n"
  },
  {
    "path": "examples/adbkit-init/README.md",
    "content": "# adbkit-init\nrun `python -m uiautomator2 init` once android device plugin.\n\n## Installation\n1. Install nodejs\n2. Install dependencies by npm\n\n    ```bash\n    npm install\n    ```\n\n## Usage\n```bash\nnode main.js --server $SERVER_ADDR\n```\n\nHow it works.\n\nUse adbkit to trace device. And the following command will call when device plugin\n\n```bash\npython -m uiautomator2 init --server $SERVER_ADDR\n```\n\n## LICENSE\nMIT\n"
  },
  {
    "path": "examples/adbkit-init/main.js",
    "content": "'use strict'\n\nvar Promise = require('bluebird')\nvar adb = require('adbkit')\nvar client = adb.createClient()\nvar util = require('util')\nconst { spawn } = require(\"child_process\")\nvar argv = require('minimist')(process.argv.slice(2))\n\nconst serverAddr = argv.server; // Usage: node main.js --server $SERVER_ADDR\n\nfunction initDevice(device) {\n  if (device.type != 'device') {\n    return\n  }\n  client.shell(device.id, 'am start -a android.intent.action.VIEW -d http://www.stackoverflow.com')\n    .then(adb.util.readAll)\n    .then(function(output) {\n      var args = [\"-m\", \"uiautomator2\", \"init\", \"--serial\", device.id]\n      if (serverAddr) {\n        args.push(\"--server\", serverAddr);\n      }\n      const child = spawn(\"python\", args);\n      child.stdout.on(\"data\", data => {\n        process.stdout.write(data)\n      })\n      child.stderr.on(\"data\", data => {\n        process.stderr.write(data)\n      })\n      child.on('close', code => {\n        util.log(`child process exited with code ${code}`);\n      });\n    })\n}\n\nutil.log(\"tracking device\")\nif (serverAddr) {\n  util.log(\"server %s\", serverAddr)\n}\n\nclient.trackDevices()\n  .then(function(tracker) {\n    tracker.on('add', function(device) {\n      util.log(\"Device %s(%s) was plugged in\", device.id, device.type)\n      initDevice(device)\n    })\n    tracker.on('remove', function(device) {\n      util.log('Device %s was unplugged', device.id)\n    })\n    tracker.on(\"change\", function(device) {\n      util.log('Device %s was changed to %s', device.id, device.type)\n      initDevice(device)\n    })\n    tracker.on('end', function() {\n      util.log('Tracking stopped')\n    })\n  })"
  },
  {
    "path": "examples/adbkit-init/package.json",
    "content": "{\n  \"name\": \"adbkit-init\",\n  \"version\": \"1.0.0\",\n  \"description\": \"run `python -m uiautomator2 init` once android device plugin.\",\n  \"main\": \"main.js\",\n  \"scripts\": {\n    \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\"\n  },\n  \"author\": \"\",\n  \"license\": \"MIT\",\n  \"dependencies\": {\n    \"adbkit\": \"^2.11.0\",\n    \"minimist\": \"^1.2.0\"\n  }\n}"
  },
  {
    "path": "examples/apk_install.py",
    "content": "# coding: utf-8\n#\n# Install problems\n#\n# OPPO need password\n\nimport time\n\nimport uiautomator2 as u2\n\n\ndef oppo_verify(u):\n    password = \"your-password\"\n    if u(packageName=\"com.coloros.safecenter\", textContains=\"请验证身份后安装\").exists:\n        print(\"Auto click install\")\n        u.set_fastinput_ime()\n        u(className='android.widget.EditText').set_text(password)\n        u(className='android.widget.Button', text='安装').click()\n        time.sleep(5)\n        u(className='android.widget.Button', text='安装').click()\n        u(className='android.widget.Button', text='完成').click()\n        return True\n\n    if u(packageName=\"com.android.packageinstaller\", text=\"重新安装\").click_exists():\n        print(\"Reinstall\")\n        u(className='android.widget.Button', text='安装').click()\n        u(className='android.widget.Button', text='完成').click()\n        return True\n\n\ndef main():\n    u = u2.connect()\n    u.open_identify()\n    u.app_install('https://some-gameapp.apk', installing_callback=oppo_verify)\n\n\n\n\n\nif __name__ == '__main__':\n    main()"
  },
  {
    "path": "examples/batteryweb/README.md",
    "content": "# batteryweb\nEasy watch device battery\n\n# Install\n```bash\npip install -r requirements.txt\n```\n\n# Usage\n\n```bash\nexport FLASK_APP=\"main.py\"\nexport FLASK_DEBUG=1\nflask run\n```\n\nDemo\n\n![img](https://testerhome.com/uploads/photo/2017/60770b8c-555b-4e54-81f5-ed4facd87ecc.png)\n"
  },
  {
    "path": "examples/batteryweb/main.py",
    "content": "# coding: utf-8\n#\n\nimport flask\nimport requests\n\napp = flask.Flask(__name__)\n\n\n@app.route('/')\ndef index():\n    return flask.render_template('index.html')\n\n\n@app.route('/battery_level/<ip>')\ndef battery_level(ip):\n    r = requests.get('http://'+ip+':7912/info').json()\n    return str(r.get('battery').get('level'))"
  },
  {
    "path": "examples/batteryweb/templates/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n  <meta charset=\"UTF-8\">\n  <meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\">\n  <title>Battery</title>\n  <script src=\"https://cdn.jsdelivr.net/npm/vue@2.5.3/dist/vue.min.js\"></script>\n  <script src=\"https://cdn.jsdelivr.net/npm/echarts@3.8.5/dist/echarts.min.js\"></script>\n  <script src=\"https://cdn.jsdelivr.net/npm/jquery@3.2.1/dist/jquery.min.js\"></script>\n</head>\n\n<body>\n  使用前需要安装\n  <a href=\"https://github.com/openatx/uiautomator2\" target=\"_blank\">uiautomator2</a>\n  <div>\n    手机IP:\n    <input id=\"ip\" type=\"text\">\n  </div>\n  <div id=\"main\" style=\"height:400px;\"></div>\n  <script type=\"text/javascript\">\n    // 基于准备好的dom，初始化echarts实例\n    var myChart = echarts.init(document.getElementById('main'));\n\n    var data = [];\n\n\n\n    // 指定图表的配置项和数据\n    var option = {\n      title: {\n        text: '电池电量'\n      },\n      tooltip: {\n        trigger: 'axis',\n        alwaysShowContent: true,\n      },\n      dataZoom: [\n        {\n          id: 'dataZoomX',\n          type: 'slider',\n          xAxisIndex: [0],\n          filterMode: 'filter'\n        }\n      ],\n      xAxis: {\n        type: 'time',\n      },\n      yAxis: {\n        type: 'value',\n        max: 100,\n        scale: true,\n        axisLabel: {\n          formatter: function (val) {\n            return val + '%';\n          }\n        },\n      },\n      series: [{\n        name: '电量',\n        type: 'line',\n        step: 'end',\n        data: data,\n      }]\n    };\n\n    var lastValue = -1;\n\n    function pushNewValue() {\n      var ip = $('#ip').val();\n      if (!ip) {\n        return;\n      }\n      $.ajax({\n        method: 'get',\n        url: '/battery_level/' + ip,\n        type: 'json',\n      })\n        .then(function (value) {\n          console.log(\"IP:\", ip, value);\n          data.pop();\n          if (lastValue != value) {\n            lastValue = value;\n            data.push([new Date(), value])\n            data.push([new Date(), value])\n          } else {\n            data.push([new Date(), value])\n          }\n\n          myChart.setOption({\n            series: [{\n              data: data\n            }]\n          });\n        })\n      // .fail(function(){\n      // })\n    }\n    setInterval(pushNewValue, 1000);\n\n    // 使用刚指定的配置项和数据显示图表。\n    myChart.setOption(option);\n\n    window.onresize = function () {\n      myChart.resize();\n    }\n  </script>\n</body>\n\n</html>"
  },
  {
    "path": "examples/com.codeskyblue.remotecamera/main_test.py",
    "content": "# coding: utf-8\n\nimport uiautomator2 as u2\n\npkg_name = 'com.codeskyblue.remotecamera'\nd = u2.connect()\n\n\ndef setup_function():\n    d.app_start(pkg_name)\n\n\ndef test_simple():\n    assert d(text=\"Hello World!\").exists\n\n"
  },
  {
    "path": "examples/com.netease.cloudmusic/README.txt",
    "content": "网易云音乐 测试用例\n================\n\n## P0用例\n- 歌曲（播放、暂停、播放）"
  },
  {
    "path": "examples/com.netease.cloudmusic/main.py",
    "content": "# coding: utf-8\n\nimport uiautomator2 as u2\n\n\ndef main():\n    u = u2.connect_usb()\n    u.app_start('com.netease.cloudmusic')\n    u(text='私人FM').click()\n    u(description='转到上一层级').click()\n    u(text='每日推荐').click()\n    u(description='转到上一层级').click()\n    u(text='歌单').click()\n    u(description='转到上一层级').click()\n    u(text='排行榜').click()\n    u(description='转到上一层级').click()\n\nif __name__ == '__main__':\n    main()"
  },
  {
    "path": "examples/minitouch.py",
    "content": "# coding: utf-8\n#\n# 半成品\nimport json\n\nfrom websocket import create_connection\n\nfrom . import Device\n\n\nclass Minitouch:\n    # TODO: need test\n    def __init__(self, d: Device):\n        self._d = d\n        self._prepare()\n\n    def _prepare(self):\n        self._w, self._h = self._d.window_size()\n        uri = self._d.path2url(\"/minitouch\").replace(\"http:\", \"ws:\")\n        self._ws = create_connection(uri)\n        # self._reset()\n\n    def down(self, x, y, index: int = 0):\n        px = x / self._w\n        py = y / self._h\n        self._ws_send({\"operation\": \"d\", \"index\": index, \"xP\": px, \"yP\": py, \"pressure\": 0.5})\n        self._commit()\n\n    def move(self, x, y, index: int = 0):\n        px = x / self._w\n        py = y / self._h\n        self._ws_send({\"operation\": \"m\", \"index\": index, \"xP\": px, \"yP\": py, \"pressure\": 0.5})\n\n    def up(self, x, y, index: int = 0):\n        self._ws_send({\"operation\": \"u\", \"index\": index})\n        self._commit()\n\n    def click(self, x, y):\n        self.down(x, y)\n        self.up(x, y)\n\n    def pinch_in(self, x, y, radius: int, steps: int = 10):\n        \"\"\"\n        Args:\n            x, y: center point\n        \"\"\"\n        pass\n\n    def _reset(self):\n        self._ws_send({\"operation\": \"r\"}) # reset\n\n    def _commit(self):\n        self._ws_send({\"operation\": \"c\"})\n\n    def _ws_send(self, payload: dict):\n        from pprint import pprint\n        pprint(payload)\n        self._ws.send(json.dumps(payload), opcode=1)\n"
  },
  {
    "path": "examples/multi-thread-example.py",
    "content": "# coding: utf-8\n#\n# GIL limit python multi-thread effectiveness.\n# But is seems fine, because these operation have so many socket IO\n# So  it seems no need to use multiprocess\n#\nimport threading\n\nimport adbutils\nfrom logzero import logger\n\nimport uiautomator2 as u2\n\n\ndef worker(d: u2.Device):\n    d.app_start(\"io.appium.android.apis\", stop=True)\n    d(text=\"App\").wait()\n    for el in d.xpath(\"@android:id/list\").child(\"/android.widget.TextView\").all():\n        logger.info(\"%s click %s\", d.serial, el.text)\n        el.click()\n        d.press(\"back\")\n    logger.info(\"%s DONE\", d.serial)\n\n\nfor dev in adbutils.adb.device_list():\n    print(\"Dev:\", dev)\n    d = u2.connect(dev.serial)\n    t = threading.Thread(target=worker, args=(d,))\n    t.start()\n"
  },
  {
    "path": "examples/runyaml/run.py",
    "content": "#!/usr/bin/env python3\n# coding: utf-8\n#\n\nimport argparse\nimport logging\nimport os\nimport re\nimport time\n\nimport bunch\nimport yaml\nfrom logzero import logger\n\nimport uiautomator2 as u2\n\nCLICK = \"click\"\n# swipe\nSWIPE_UP = \"swipe_up\"\nSWIPE_RIGHT = \"swipe_right\"\nSWIPE_LEFT = \"swipe_left\"\nSWIPE_DOWN = \"swipe_down\"\n\nSCREENSHOT = \"screenshot\"\nEXIST = \"assert_exist\"\nWAIT = \"wait\"\n\n\ndef split_step(text: str):\n    __alias = {\n        \"点击\": CLICK,\n        \"上滑\": SWIPE_UP,\n        \"右滑\": SWIPE_RIGHT,\n        \"左滑\": SWIPE_LEFT,\n        \"下滑\": SWIPE_DOWN,\n        \"截图\": SCREENSHOT,\n        \"存在\": EXIST,\n        \"等待\": WAIT,\n    }\n\n    for keyword in __alias.keys():\n        if text.startswith(keyword):\n            body = text[len(keyword):].strip()\n            return __alias.get(keyword, keyword), body\n    else:\n        raise RuntimeError(\"Step unable to parse\", text)\n\n\ndef read_file_content(path: str, mode:str = \"r\") -> str:\n    with open(path, mode) as f:\n        return f.read()\n\ndef run_step(cf: bunch.Bunch, app: u2.Device, step: str):\n    logger.info(\"Step: %s\", step)\n    oper, body = split_step(step)\n    logger\n    \n    logger = logging.getLogger(__name__).debug(\"parse as: %s %s\", oper, body)\n\n    if oper == CLICK:\n        app.xpath(body).click()\n\n    elif oper == SWIPE_RIGHT:\n        app.xpath(body).swipe(\"right\")\n    elif oper == SWIPE_UP:\n        app.xpath(body).swipe(\"up\")\n    elif oper == SWIPE_LEFT:\n        app.xpath(body).swipe(\"left\")\n    elif oper == SWIPE_DOWN:\n        app.xpath(body).swipe(\"down\")\n\n    elif oper == SCREENSHOT:\n        output_dir = \"./output\"\n        filename = \"screen-%d.jpg\" % int(time.time()*1000)\n        if body:\n            filename = body\n        name_noext, ext = os.path.splitext(filename)\n        if ext.lower() not in ['.jpg', '.jpeg', '.png']:\n            ext = \".jpg\"\n        os.makedirs(cf.output_directory, exist_ok=True)\n        filename = os.path.join(cf.output_directory, name_noext + ext)\n        logger.debug(\"Save screenshot: %s\", filename)\n        app.screenshot().save(filename)\n\n    elif oper == EXIST:\n        assert app.xpath(body).wait(), body\n\n    elif oper == WAIT:\n        #if re.match(\"^[\\d\\.]+$\")\n        if body.isdigit():\n            seconds = int(body)\n            logger.info(\"Sleep %d seconds\", seconds)\n            time.sleep(seconds)\n        else:\n            app.xpath(body).wait()\n\n    else:\n        raise RuntimeError(\"Unhandled operation\", oper)\n    \n\ndef run_conf(d, conf_filename: str):\n    d.healthcheck()\n    d.xpath.when(\"允许\").click()\n    d.xpath.watch_background(2.0)\n\n    cf = yaml.load(read_file_content(conf_filename), Loader=yaml.SafeLoader)\n    default = {\n        \"output_directory\": \"output\",\n        \"action_before_delay\": 0,\n        \"action_after_delay\": 0,\n        \"skip_cleanup\": False,\n    }\n    for k, v in default.items():\n        cf.setdefault(k, v)\n    cf = bunch.Bunch(cf)\n\n    print(\"Author:\", cf.author)\n    print(\"Description:\", cf.description)\n    print(\"Package:\", cf.package)\n    logger.debug(\"action_delay: %.1f / %.1f\", cf.action_before_delay, cf.action_after_delay)\n\n    app = d.session(cf.package)\n    for step in cf.steps:\n        time.sleep(cf.action_before_delay)\n        run_step(cf, app, step)\n        time.sleep(cf.action_after_delay)\n\n    if not cf.skip_cleanup:\n        app.close()\n\n\ndevice = None\nconf_filename = None\n\ndef test_entry():\n    pass\n\n\n\nif __name__ == \"__main__\":\n    parser = argparse.ArgumentParser()\n    parser.add_argument(\"-c\", \"--command\", help=\"run single step command\")\n    parser.add_argument(\"-s\", \"--serial\", help=\"run single step command\")\n    parser.add_argument(\"conf_filename\", default=\"test.yml\", nargs=\"?\", help=\"config filename\")\n    args = parser.parse_args()\n\n    d = u2.connect(args.serial)\n    if args.command:\n        cf = bunch.Bunch({\"output_directory\": \"output\"})\n        app = d.session()\n        run_step(cf, app, args.command)\n\n    else:\n        run_conf(d, args.conf_filename)\n"
  },
  {
    "path": "examples/runyaml/test.yml",
    "content": "---\nauthor: shengxiang.ssx 圣翔\ndescription: 扫一扫测试\npackage: com.taobao.taobao\nlink: https://aone.xxx.com/xxxx\noutput_directory: output # optional [default \"output\"]\naction_before_delay: 0.5 # optional [default 0]\naction_after_delay: 1 # optional [default 0]\nskip_cleanup: false # optional [default true]\nsteps:\n- 等待 我的淘宝\n- 点击 扫一扫\n- 点击 拍立淘\n- 截图\n- 存在 扫一扫\n"
  },
  {
    "path": "examples/test_simple_example.py",
    "content": "# coding: utf-8\n#\n\nimport uiautomator2 as u2\n\n\ndef test_simple():\n    d = u2.connect()\n    print(d.info)\n\n\nif __name__ == \"__main__\":\n    test_simple()\n"
  },
  {
    "path": "examples/u2iniit-standalone/README.txt",
    "content": "## atx uiautomator2-init standalone\n该版本不用联网下载依赖\n\n## 使用方法\n使用notepad打开`设备初始化.bat` 修改其中的atx-server地址\n\n1. 双击脚本\n2. 插入安卓手机，会自动启动初始化步骤。等待控制台出现Init Success表示成功\n\n## 更新历史\n- 2018/03/30 第一版创建\n"
  },
  {
    "path": "examples/u2iniit-standalone/init-vendor.sh",
    "content": "#!/bin/bash -\n#\n\nset -e\n\n# Verisons modify here\nATX_AGENT_VERSION=0.3.0\nUIAUTOMATOR_APK_VERSION=1.0.13\n\n# Download resources\nmkdir -p vendor\n\ndownload_apk(){\n        APK_BASE_URL=\"https://github.com/openatx/android-uiautomator-server/releases/download/${UIAUTOMATOR_APK_VERSION}\"\n        wget -O vendor/app-uiautomator.apk \"$APK_BASE_URL/app-uiautomator.apk\"\n        wget -O vendor/app-uiautomator-test.apk \"$APK_BASE_URL/app-uiautomator-test.apk\"\n}\n\ndownload_stf(){\n        ## minicap+minitouch\n        wget -O vendor/stf-binaries.zip \"https://github.com/codeskyblue/stf-binaries/archive/master.zip\"\n        unzip -o -d vendor/ vendor/stf-binaries.zip\n}\n\ndownload_atx(){\n        ## atx-agent\n        wget -O vendor/atx-agent-$ATX_AGENT_VERSION.tar.gz \"https://github.com/openatx/atx-agent/releases/download/$ATX_AGENT_VERSION/atx-agent_${ATX_AGENT_VERSION}_linux_armv6.tar.gz\"\n        tar -C vendor/ -xzvf vendor/atx-agent-$ATX_AGENT_VERSION.tar.gz atx-agent\n}\n\ndownload_atx\ndownload_apk\ndownload_stf\n\necho \"Everything is downloaded. ^_^\""
  },
  {
    "path": "examples/u2iniit-standalone/main.go",
    "content": "package main\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\tgoadb \"github.com/yosemite-open/go-adb\"\n)\n\nvar adb *goadb.Adb\n\nconst stfBinariesDir = \"vendor/stf-binaries-master/node_modules\"\n\nfunc init() {\n\tvar err error\n\tadb, err = goadb.New()\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tserverVersion, err := adb.ServerVersion()\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tfmt.Printf(\"adb server version: %d\\n\", serverVersion)\n}\n\nfunc initUiAutomator2(device *goadb.Device, serverAddr string) error {\n\tprops, err := device.Properties()\n\tif err != nil {\n\t\treturn err\n\t}\n\tsdk := props[\"ro.build.version.sdk\"]\n\tabi := props[\"ro.product.cpu.abi\"]\n\tpre := props[\"ro.build.version.preview_sdk\"]\n\t// arch := props[\"ro.arch\"]\n\tlog.Printf(\"product model: %s\\n\", props[\"ro.product.model\"])\n\n\tif pre != \"\" && pre != \"0\" {\n\t\tsdk += pre\n\t}\n\tlog.Println(\"Install minicap and minitouch\")\n\tif err := initSTFMiniTools(device, abi, sdk); err != nil {\n\t\treturn errors.Wrap(err, \"mini(cap|touch)\")\n\t}\n\tlog.Println(\"Install app-uiautomator[-test].apk\")\n\tif err := initUiAutomatorAPK(device); err != nil {\n\t\treturn errors.Wrap(err, \"app-uiautomator[-test].apk\")\n\t}\n\tlog.Println(\"Install atx-agent\")\n\tatxAgentPath := \"vendor/atx-agent\"\n\tif err := writeFileToDevice(device, atxAgentPath, \"/data/local/tmp/atx-agent\", 0755); err != nil {\n\t\treturn errors.Wrap(err, \"atx-agent\")\n\t}\n\n\targs := []string{\"-d\"}\n\tif serverAddr != \"\" {\n\t\targs = append(args, \"-t\", serverAddr)\n\t}\n\toutput, err := device.RunCommand(\"/data/local/tmp/atx-agent\", args...)\n\toutput = strings.TrimSpace(output)\n\tif err != nil {\n\t\treturn errors.Wrap(err, \"start atx-agent\")\n\t}\n\tserial, _ := device.Serial()\n\tfmt.Println(serial, output)\n\treturn nil\n}\n\nfunc writeFileToDevice(device *goadb.Device, src, dst string, mode os.FileMode) error {\n\tf, err := os.Open(src)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer f.Close()\n\tdstTemp := dst + \".tmp-magic1231x\"\n\t_, err = device.WriteToFile(dstTemp, f, mode)\n\tif err != nil {\n\t\tdevice.RunCommand(\"rm\", dstTemp)\n\t\treturn err\n\t}\n\t// use mv to prevent \"text busy\" error\n\t_, err = device.RunCommand(\"mv\", dstTemp, dst)\n\treturn err\n}\n\nfunc initMiniTouch(device *goadb.Device, abi string) error {\n\tsrcPath := fmt.Sprintf(stfBinariesDir+\"/minitouch-prebuilt/prebuilt/%s/bin/minitouch\", abi)\n\treturn writeFileToDevice(device, srcPath, \"/data/local/tmp/minitouch\", 0755)\n}\n\nfunc initSTFMiniTools(device *goadb.Device, abi, sdk string) error {\n\tsoSrcPath := fmt.Sprintf(stfBinariesDir+\"/minicap-prebuilt/prebuilt/%s/lib/android-%s/minicap.so\", abi, sdk)\n\terr := writeFileToDevice(device, soSrcPath, \"/data/local/tmp/minicap.so\", 0644)\n\tif err != nil {\n\t\treturn err\n\t}\n\tbinSrcPath := fmt.Sprintf(stfBinariesDir+\"/minicap-prebuilt/prebuilt/%s/bin/minicap\", abi)\n\terr = writeFileToDevice(device, binSrcPath, \"/data/local/tmp/minicap\", 0755)\n\tif err != nil {\n\t\treturn err\n\t}\n\ttouchSrcPath := fmt.Sprintf(stfBinariesDir+\"/minitouch-prebuilt/prebuilt/%s/bin/minitouch\", abi)\n\treturn writeFileToDevice(device, touchSrcPath, \"/data/local/tmp/minitouch\", 0755)\n}\n\nfunc installAPK(device *goadb.Device, localPath string) error {\n\tdstPath := \"/data/local/tmp/\" + filepath.Base(localPath)\n\tif err := writeFileToDevice(device, localPath, dstPath, 0644); err != nil {\n\t\treturn err\n\t}\n\tdefer device.RunCommand(\"rm\", dstPath)\n\toutput, err := device.RunCommand(\"pm\", \"install\", \"-r\", \"-t\", dstPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !strings.Contains(output, \"Success\") {\n\t\treturn errors.Wrap(errors.New(output), \"apk-install\")\n\t}\n\treturn nil\n}\n\nfunc initUiAutomatorAPK(device *goadb.Device) (err error) {\n\t_, er1 := device.StatPackage(\"com.github.uiautomator\")\n\t_, er2 := device.StatPackage(\"com.github.uiautomator.test\")\n\tif er1 == nil && er2 == nil {\n\t\tlog.Println(\"APK already installed, Skip. Uninstall apk manually if you want to reinstall apk\")\n\t\treturn\n\t}\n\terr = installAPK(device, \"vendor/app-uiautomator.apk\")\n\tif err != nil {\n\t\treturn\n\t}\n\treturn installAPK(device, \"vendor/app-uiautomator-test.apk\")\n}\n\nfunc startService(device *goadb.Device) (err error) {\n\t_, err = device.RunCommand(\"am\", \"startservice\", \"-n\", \"com.github.uiautomator/.Service\")\n\treturn err\n}\n\nfunc watchAndInit(serverAddr string) {\n\twatcher := adb.NewDeviceWatcher()\n\tfor event := range watcher.C() {\n\t\tif event.CameOnline() {\n\t\t\tlog.Printf(\"Device %s came online\", event.Serial)\n\t\t\tdevice := adb.Device(goadb.DeviceWithSerial(event.Serial))\n\t\t\tlog.Printf(\"Init device\")\n\t\t\tif err := initUiAutomator2(device, serverAddr); err != nil {\n\t\t\t\tlog.Printf(\"Init error: %v\", err)\n\t\t\t\tcontinue\n\t\t\t} else {\n\t\t\t\tlog.Printf(\"Init Success\")\n\t\t\t\tstartService(device)\n\t\t\t}\n\t\t}\n\t\tif event.WentOffline() {\n\t\t\tlog.Printf(\"Device %s went offline\", event.Serial)\n\t\t}\n\t}\n\tif watcher.Err() != nil {\n\t\tlog.Fatal(watcher.Err())\n\t}\n}\n\nfunc main() {\n\tserverAddr := flag.String(\"server\", \"\", \"atx-server address(must be ip:port) eg: 10.0.0.1:7700\")\n\tflag.Parse()\n\n\tfmt.Println(\"u2init version 20180330\")\n\twd, _ := os.Getwd()\n\tlog.Println(\"Add adb.exe to PATH +=\", filepath.Join(wd, \"vendor\"))\n\tnewPath := fmt.Sprintf(\"%s%s%s\", os.Getenv(\"PATH\"), string(os.PathListSeparator), filepath.Join(wd, \"vendor\"))\n\tos.Setenv(\"PATH\", newPath)\n\n\twatchAndInit(*serverAddr)\n}\n"
  },
  {
    "path": "examples/u2iniit-standalone/proxyhttp.go",
    "content": "package main\n\nimport (\n\t\"log\"\n\t\"net/http\"\n\t\"net/http/httputil\"\n\t\"net/url\"\n\n\t\"github.com/koding/websocketproxy\"\n)\n\n// TODO(ssx): not tested yet.\ntype HTTPWSProxy struct {\n\tforwardedAddr string\n\twsProxy       *websocketproxy.WebsocketProxy\n\thttpProxy     *httputil.ReverseProxy\n}\n\n// NewHTTPWSProxy return Proxy instance\nfunc NewHTTPWSProxy(forwardedAddr string) *HTTPWSProxy {\n\twsURL, _ := url.Parse(\"ws://\" + forwardedAddr)\n\thttpURL, _ := url.Parse(\"http://\" + forwardedAddr)\n\n\treturn &HTTPWSProxy{\n\t\tforwardedAddr: forwardedAddr,\n\t\twsProxy:       websocketproxy.NewProxy(wsURL),\n\t\thttpProxy:     httputil.NewSingleHostReverseProxy(httpURL),\n\t}\n}\n\nfunc (p *HTTPWSProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {\n\tif r.Header.Get(\"Upgrade\") == \"websocket\" {\n\t\tlog.Println(\"proxy websocket\", r.RequestURI)\n\t\tp.wsProxy.ServeHTTP(w, r)\n\t\treturn\n\t}\n\tlog.Println(\"proxy http\", r.RequestURI)\n\tp.httpProxy.ServeHTTP(w, r)\n}\n"
  },
  {
    "path": "examples/u2iniit-standalone/uiautomator2-init-standalone.bat",
    "content": "@echo off\n\necho \"ֻԶʼ\"\nu2init.exe\n\nrem Ҫatx-server, עӵ\nrem u2init.exe -server \"REPLACE-ATX-SERVER-ADDR-HERE\"\n\npause\n"
  },
  {
    "path": "mobile_tests/conftest.py",
    "content": "# coding: utf-8\n\nimport adbutils\nimport pytest\n\nimport uiautomator2 as u2\n\n\n@pytest.fixture(scope=\"module\")\ndef d(device):\n    _d = device\n    _d.settings['operation_delay'] = (0.2, 0.2)\n    _d.settings['operation_delay_methods'] = ['click', 'swipe']\n    return _d\n\n\n@pytest.fixture\ndef package_name():\n    return \"io.appium.android.apis\"\n\n\n@pytest.fixture(scope=\"function\")\ndef dev(d: u2.Device, package_name) -> u2.Device: # type: ignore\n    d.watcher.reset()\n    \n    d.app_start(package_name, stop=True)\n    yield d\n\n\n# run parallel\n# py.test --tx \"3*popen\" --dist=load test_device.py -q --tb=line\n\n#def read_device_list() -> list:\n#    return [v.serial for v in adbutils.adb.device_list()]\n\n\n#def pytest_configure(config):\n#     # read device list if we are on the master\n#     if not hasattr(config, \"slaveinput\"):\n#        config.devlist = read_device_list()\n\n\n# def pytest_configure_node(node):\n#     # the master for each node fills slaveinput dictionary\n#     # which pytest-xdist will transfer to the subprocess\n#     serial = node.slaveinput[\"serial\"] = node.config.devlist.pop()\n#     node.config.devlist.insert(0, serial)\n\n\n@pytest.fixture(scope=\"session\")\ndef device(request):\n    return u2.connect()\n    # slaveinput = getattr(request.config, \"slaveinput\", None)\n    # if slaveinput is None: # single-process execution\n    #     serial = read_device_list()[0]\n    # else: # running in a subprocess here\n    #     serial = slaveinput[\"serial\"]\n    # print(\"SERIAL:\", serial)\n    # return u2.connect(serial)\n"
  },
  {
    "path": "mobile_tests/runtest.sh",
    "content": "#!/bin/bash\n#\n\nset -e\n\nif [[ $# -eq 0 ]]\nthen\n\tURL=\"https://github.com/appium/java-client/raw/v7.3.0/src/test/java/io/appium/java_client/ApiDemos-debug.apk\"\n\tpython3 -m adbutils -i \"$URL\" #https://github.com/appium/java-client/raw/master/src/test/java/io/appium/java_client/ApiDemos-debug.apk\nfi\npy.test -v \"$@\"\n"
  },
  {
    "path": "mobile_tests/skip_test_image.py",
    "content": "# coding: utf-8\n#\n\nimport os\n\nimport cv2\nimport numpy as np\nimport pytest\nfrom PIL import Image\n\nimport uiautomator2.image as u2image\n\nTESTDIR = os.path.dirname(os.path.abspath(__file__)) + \"/testdata\" # if set to DIR__, pytest will fail with TypeError: 'str' object is not callable\n\n@pytest.fixture\ndef path_ae86():\n    filepath = os.path.join(TESTDIR, \"./AE86.jpg\")\n    return filepath\n\n\n@pytest.fixture\ndef im_ae86(path_ae86: str) -> np.ndarray:\n    \"\"\" 使用opencv打开的图片 \"\"\"\n    im = cv2.imread(path_ae86)\n    return im\n\n\ndef test_imread(im_ae86, path_ae86):\n    # Path\n\n    im = u2image.imread(path_ae86)\n    assert im.shape == (193, 321, 3)\n\n    # URL\n    im = u2image.imread(\"https://www.baidu.com/img/bd_logo1.png\")\n    assert im.shape == (258, 540, 3)\n\n    # Opencv\n    im = u2image.imread(im_ae86)\n    assert im.shape == (193, 321, 3), \"图片格式变化\"\n\n    # PIL.Image\n    pilim = Image.open(path_ae86)\n    im = u2image.imread(pilim)\n    assert pilim.size == (321, 193)\n    assert im.shape == (193, 321, 3), \"图片格式变化\"\n\n\n@pytest.mark.skip(\"missing test images\")\ndef test_image_match():\n    class MockDevice():\n        def __init__(self):\n            self.x = None\n            self.y = None\n\n        def click(self, x, y):\n            self.x = x\n            self.y = y\n\n        def screenshot(self, *args, **kwargs):\n            return cv2.imread(TESTDIR + \"/screenshot.jpg\")\n\n    d = MockDevice()\n    ix = u2image.ImageX(d)\n    template = Image.open(TESTDIR + \"/template.jpg\")\n    res = ix.match(template)\n    \n    x, y = res['point']\n    assert (x, y) == (409, 659), \"Match position is wrong\"\n    \n    ix.click(template)\n    assert d.x == 409\n    assert d.y == 659\n\n    if False: # show position\n        pim = Image.open(TESTDIR+\"/screenshot.jpg\")\n        nim = u2image.draw_point(pim, x, y)\n        nim.show()\n\n"
  },
  {
    "path": "mobile_tests/test_push_pull.py",
    "content": "# coding: utf-8\n#\n\nimport io\nimport os\n\nimport uiautomator2 as u2\n\n\ndef test_push_and_pull(d: u2.Device):\n    device_target = \"/data/local/tmp/hello.txt\"\n    content = b\"hello world\"\n\n    d.push(io.BytesIO(content), device_target)\n    d.pull(device_target, \"tmpfile-hello.txt\")\n    with open(\"tmpfile-hello.txt\", \"rb\") as f:\n        assert f.read() == content\n    os.unlink(\"tmpfile-hello.txt\")\n"
  },
  {
    "path": "mobile_tests/test_screenrecord.py",
    "content": "# coding: utf-8\n#\n\nimport time\n\nimport pytest\n\nimport uiautomator2 as u2\n\n\n@pytest.mark.skip(\"deprecated\")\ndef test_screenrecord(d: u2.Device):\n    import imageio\n    with pytest.raises(RuntimeError):\n        d.screenrecord.stop()\n\n    d.screenrecord(\"output.mp4\", fps=10)\n    start = time.time()\n\n    with pytest.raises(RuntimeError):\n        d.screenrecord(\"output2.mp4\")\n\n    time.sleep(3.0)\n    d.screenrecord.stop()\n    print(\"Time used:\", time.time() - start)\n\n    # check\n    with imageio.get_reader(\"output.mp4\") as f:\n        meta = f.get_meta_data()\n        assert isinstance(meta, dict)\n        from pprint import pprint\n        pprint(meta)"
  },
  {
    "path": "mobile_tests/test_session.py",
    "content": "# coding: utf-8\n#\n\nimport pytest\n\nimport uiautomator2 as u2\nfrom uiautomator2.exceptions import SessionBrokenError\n\n\ndef test_session_function_exists(dev: u2.Device):\n    dev.wlan_ip\n    dev.watcher\n    dev.jsonrpc\n    dev.shell\n    dev.settings\n    dev.xpath\n\n\ndef test_app_mixin(dev: u2.Device, package_name: str):\n    assert package_name in dev.app_list()\n    dev.app_stop(package_name)\n    assert package_name not in dev.app_list_running()\n    dev.app_start(package_name)\n    assert package_name in dev.app_list_running()\n    \n    demo_pid = dev.app_wait(package_name)\n    current_info = dev.app_current()\n    assert demo_pid == current_info['pid']\n    assert current_info['package'] == package_name\n\n    dev.app_start(package_name, stop=True)\n    assert demo_pid != dev.app_wait(package_name)\n\n\ndef test_session_app(dev: u2.Device, package_name):\n    dev.app_start(package_name)\n    assert dev.app_current()['package'] == package_name\n\n    dev.app_wait(package_name)\n    assert package_name in dev.app_list()\n    assert package_name in dev.app_list_running()\n\n    with dev.session(\"io.appium.android.apis\") as sess:\n        sess(text=\"App\").click()\n        assert sess.running() is True\n        dev.app_stop(\"io.appium.android.apis\")\n        assert sess.running() is False\n        with pytest.raises(SessionBrokenError):\n            sess(text=\"App\").click()\n    \n    with dev.session(\"io.appium.android.apis\") as sess:\n        sess(text=\"App\").click()\n        assert sess.running() is True\n\n\ndef test_session_window_size(dev: u2.Device):\n    assert isinstance(dev.window_size(), tuple)\n\n\ndef test_auto_grant_permissions(dev: u2.Device):\n    dev.app_auto_grant_permissions(\"io.appium.android.apis\")\n\n"
  },
  {
    "path": "mobile_tests/test_settings.py",
    "content": "# coding: utf-8\n#\n\nimport time\n\nimport pytest\n\nimport uiautomator2 as u2\n\n\ndef test_set_xpath_debug(dev: u2.Device):\n    with pytest.raises(TypeError):\n        dev.settings['xpath_debug'] = 1\n    \n    dev.settings['xpath_debug'] = True\n    assert dev.settings['xpath_debug'] == True\n\n    dev.settings['xpath_debug'] = False\n    assert dev.settings['xpath_debug'] == False\n\n\ndef test_wait_timeout(d: u2.Device):\n    d.settings['wait_timeout'] = 19.0\n    assert d.wait_timeout == 19.0\n\n    d.settings['wait_timeout'] = 10\n    assert d.wait_timeout == 10\n\n    d.implicitly_wait(15)\n    assert d.settings['wait_timeout'] == 15\n\n\ndef test_operation_delay(dev: u2.Session):\n    x, y = dev(text=\"App\").center()\n\n    # 测试前延迟\n    start = time.time()\n    dev.settings['operation_delay'] = (1, 0)\n    dev.click(x, y)\n    time_used = time.time() - start\n    assert 1 < time_used < 1.5\n    \n    # 测试后延迟\n    start = time.time()\n    dev.settings['operation_delay_methods'] = ['press', 'click']\n    dev.settings['operation_delay'] = (0, 2)\n    dev.press(\"back\")\n    time_used = time.time() - start\n    # assert time_used > 2\n    #2 < time_used < 2.5\n\n    # 测试operation_delay_methods\n    start = time.time()\n    dev.settings['operation_delay_methods'] = ['press']\n    # dev.jsonrpc = Mock()\n    dev.click(x, y)\n    time_used = time.time() - start\n    # assert 0 < time_used < 0.5\n"
  },
  {
    "path": "mobile_tests/test_simple.py",
    "content": "# coding: utf-8\n#\n# Test apk Download from\n# https://github.com/appium/java-client/raw/master/src/test/java/io/appium/java_client/ApiDemos-debug.apk\n\nimport time\nimport unittest\n\nimport pytest\n\nimport uiautomator2 as u2\n\n\n@pytest.mark.skip(\"not working\")\ndef test_toast_get_message(dev: u2.Device):\n    d = dev\n    assert d.toast.get_message(0) is None\n    assert d.toast.get_message(0, default=\"d\") == \"d\"\n    d(text=\"App\").click()\n    d(text=\"Notification\").click()\n    d(text=\"NotifyWithText\").click()\n    try:\n        d(text=\"Show Short Notification\").click()\n    except u2.UiObjectNotFoundError:\n        d(text=\"SHOW SHORT NOTIFICATION\").click()\n    #self.assertEqual(d.toast.get_message(2, 5, \"\"), \"Short notification\")\n    assert \"Short notification\" in d.toast.get_message(2, 5, \"\")\n    time.sleep(.5)\n    assert d.toast.get_message(0, 0.4)\n\n\ndef test_scroll(dev: u2.Device):\n    d = dev\n    d(text=\"App\").click()\n    if not d(scrollable=True).exists:\n        pytest.skip(\"screen to large, no need to scroll\")\n    d(scrollable=True).scroll.to(text=\"Voice Recognition\")\n\n\n@pytest.mark.skip(\"Need upgrade\")\ndef test_watchers(self):\n    \"\"\"\n    App -> Notification -> Status Bar\n    \"\"\"\n    d = self.sess\n    d.watcher.remove()\n    d.watcher.stop()\n\n    d(text=\"App\").click()\n    d.xpath(\"Notification\").wait()\n    \n    d.watcher(\"N\").when('Notification').click()\n    d.watcher.run()\n\n    self.assertTrue(d(text=\"Status Bar\").wait(timeout=3))\n    d.press(\"back\")\n    d.press(\"back\")\n    # Should auto click Notification when show up\n    self.assertFalse(d.watcher.running())\n    d.watcher.start()\n\n    self.assertTrue(d.watcher.running())\n    d(text=\"App\").click()\n    self.assertTrue(d(text=\"Status Bar\").exists(timeout=5))\n\n    d.watcher.remove(\"N\")\n    d.press(\"back\")\n    d.press(\"back\")\n\n    d(text=\"App\").click()\n    self.assertFalse(d(text=\"Status Bar\").wait(timeout=5))\n\n\n@pytest.mark.skip(\"TODO:: not fixed\")\ndef test_count(self):\n    d = self.sess\n    count = d(resourceId=\"android:id/list\").child(\n        className=\"android.widget.TextView\").count\n    self.assertEqual(count, 11)\n    self.assertEqual(\n        d(resourceId=\"android:id/list\").info['childCount'], 11)\n    count = d(resourceId=\"android:id/list\").child(\n        className=\"android.widget.TextView\", instance=0).count\n    self.assertEqual(count, 1)\n\ndef test_get_text(dev):\n    d = dev\n    text = d(resourceId=\"android:id/list\").child(\n        className=\"android.widget.TextView\", text=\"App\").get_text()\n    assert text == \"App\"\n\n\ndef test_xpath(dev):\n    d = dev\n    d.xpath(\"//*[@text='Media']\").wait()\n    assert len(d.xpath(\"//*[@text='Media']\").all()) == 1\n    assert len(d.xpath(\"//*[@text='MediaNotExists']\").all()) == 0\n    d.xpath(\"//*[@text='Media']\").click()\n    assert d.xpath('//*[contains(@text, \"VideoView\")]').wait(5)\n\n\n@pytest.mark.skip(\"Need fix\")\ndef test_implicitly_wait(d):\n    d.implicitly_wait(2)\n    assert d.implicitly_wait() == 2\n    start = time.time()\n    with self.assertRaises(u2.UiObjectNotFoundError):\n        d(text=\"Sensors\").get_text()\n    time_used = time.time() - start\n    assert time_used >= 2\n    # maybe longer then 2, waitForExists -> getText\n    # getText may take 1~2s\n    assert time_used < 2 + 3\n\n@pytest.mark.skip(\"TODO:: not fixed\")\ndef test_select_iter(d):\n    d(text='OS').click()\n    texts = d(resourceId='android:id/list').child(\n        className='android.widget.TextView')\n    assert texts.count == 4\n    words = []\n    for item in texts:\n        words.append(item.get_text())\n    assert words == ['Morse Code', 'Rotation Vector', 'Sensors', 'SMS Messaging']\n\n\n@pytest.mark.skip(\"Deprecated\")\ndef test_plugin(self):\n    def _my_plugin(d, k):\n        def _inner():\n            return k\n\n        return _inner\n\n    u2.plugin_clear()\n    u2.plugin_register('my', _my_plugin, 'pp')\n    self.assertEqual(self.d.ext_my(), 'pp')\n\ndef test_send_keys(dev):\n    d = dev\n    d.xpath(\"App\").click()\n    d.xpath(\"Search\").click()\n    d.xpath('//*[@text=\"Invoke Search\"]').click()\n    d.xpath('@io.appium.android.apis:id/txt_query_prefill').click()\n    d.send_keys(\"hello\", clear=True)\n    assert d.xpath('io.appium.android.apis:id/txt_query_prefill').info['text'] == 'hello'\n\n\nif __name__ == '__main__':\n    unittest.main()\n"
  },
  {
    "path": "mobile_tests/test_swipe.py",
    "content": "# coding: utf-8\n#\n\nimport time\n\nimport uiautomator2 as u2\n\n\ndef test_swipe_duration(d: u2.Device):\n    w, h = d.window_size()\n    start = time.time()\n    d.swipe(w//2, h//2, w-1, h//2, 2.0)\n    duration = time.time() - start\n    assert duration >= 1.5 # actually duration is about 7s in my TT\n"
  },
  {
    "path": "mobile_tests/test_watcher.py",
    "content": "# coding: utf-8\n#\n\nimport uiautomator2 as u2\n\n\ndef test_watch_context(dev: u2.Device):\n    with dev.watch_context(builtin=True) as ctx:\n        ctx.when(\"App\").click()\n        \n        dev(text='Menu').click()\n        assert dev(text='Inflate from XML').wait()\n\n\ndef teardown_function(d: u2.Device):\n    print(\"Teardown\", d)\n"
  },
  {
    "path": "mobile_tests/test_xpath.py",
    "content": "# coding: utf-8\n#\n\nimport threading\nfrom functools import partial\n\nimport pytest\n\nimport uiautomator2 as u2\n\n\ndef test_get_text(dev: u2.Device):\n    assert dev.xpath(\"App\").get_text() == \"App\"\n\n\ndef test_click(dev: u2.Device):\n    dev.xpath(\"App\").click()\n    assert dev.xpath(\"Alarm\").wait()\n    assert dev.xpath(\"Alarm\").exists\n\n\ndef test_swipe(dev: u2.Device):\n    d = dev\n    d.xpath(\"App\").click()\n    d.xpath(\"Alarm\").wait()\n    # assert not d.xpath(\"Voice Recognition\").exists\n    d.xpath(\"@android:id/list\").get().swipe(\"up\", 0.5)\n    assert d.xpath(\"Voice Recognition\").wait()\n\n\ndef test_xpath_query(dev: u2.Device):\n    assert dev.xpath(\"Accessibility\").wait()\n    assert dev.xpath(\"%ccessibility\").wait()\n    assert dev.xpath(\"Accessibilit%\").wait()\n\n\ndef test_element_all(dev: u2.Device):\n    app = dev.xpath('//*[@text=\"App\"]')\n    assert app.wait()\n    assert len(app.all()) == 1\n    assert app.exists\n\n\ndef test_watcher(dev: u2.Device, request):\n    dev.watcher.when(\"App\").click()\n    dev.watcher.start(interval=1.0)\n\n    event = threading.Event()\n\n    def _set_event(e):\n        e.set()\n\n    dev.watcher.when(\"Action Bar\").call(partial(_set_event, event))\n    assert event.wait(5.0), \"xpath not trigger callback\"\n\n\ndef test_xpath_scroll_to(dev: u2.Device):\n    d = dev\n    d.xpath(\"Graphics\").click()\n    d.xpath(\"@android:id/list\").scroll_to(\"Pictures\")\n    assert d.xpath(\"Pictures\").exists\n\n\ndef test_xpath_parent(dev: u2.Device):\n    d = dev\n    info = d.xpath(\"App\").parent(\"@android:id/list\").info\n    assert info[\"resourceId\"] == \"android:id/list\"\n"
  },
  {
    "path": "poetry.toml",
    "content": "[virtualenvs]\ncreate = true\nin-project = true\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[tool.poetry]\nname = \"uiautomator2\"\nversion = \"3.2.0\"\ndescription = \"uiautomator for android device\"\nhomepage = \"https://github.com/openatx/uiautomator2\"\nauthors = [\"codeskyblue <codeskyblue@gmail.com>\"]\nlicense = \"MIT\"\nreadme = \"README.md\"\ninclude = [\"*/assets/*\"]\n\n[tool.poetry.dependencies]\npython = \"^3.8\"\nrequests = \"*\"\nlxml = \"*\"\nadbutils = \">=2.11.0,<3\"\nPillow = \"*\"\nretry2 = \"^0.9.5\"\nimportlib-resources = {version = \"*\", markers = \"python_version < \\\"3.9\\\"\"}\n\n[tool.poetry.group.dev.dependencies]\npytest = \"^8.1.1\"\nisort = \"^5.13.2\"\npytest-cov = \"^4.1.0\"\nipython = \"*\"\ncoverage = \"^7.6.0\"\n\n[tool.poetry.scripts]\nuiautomator2 = \"uiautomator2.__main__:main\"\n\n[tool.poetry-dynamic-versioning] # 根据tag来动态配置版本号\nenable = true\npattern = \"^((?P<epoch>\\\\d+)!)?(?P<base>\\\\d+(\\\\.\\\\d+)*)\"\n\n[tool.poetry-dynamic-versioning.substitution]\nfiles = [\"uiautomator2/version.py\"]\n\n[build-system]\nrequires = [\"poetry-core>=1.0.0\", \"poetry-dynamic-versioning>=1.0.0,<2.0.0\"]\nbuild-backend = \"poetry_dynamic_versioning.backend\"\n"
  },
  {
    "path": "tests/test_core.py",
    "content": "# coding: utf-8\n#\n\nimport hashlib\nfrom unittest.mock import Mock, mock_open, patch\n\nimport pytest\n\nfrom uiautomator2.core import BasicUiautomatorServer\n\n\n@pytest.fixture\ndef mock_server():\n    \"\"\"Create a mock BasicUiautomatorServer instance with a mock device\"\"\"\n    mock_dev = Mock()\n    with patch.object(BasicUiautomatorServer, '__init__', return_value=None):\n        server = BasicUiautomatorServer(None)\n        server._dev = mock_dev\n        yield server, mock_dev\n\n\nclass TestCheckDeviceFileHash:\n    \"\"\"Test the _check_device_file_hash method with toybox fallback\"\"\"\n    \n    def test_toybox_md5sum_success(self, mock_server):\n        \"\"\"Test when toybox md5sum command works correctly\"\"\"\n        server, mock_dev = mock_server\n        \n        # Create a temporary file with known content\n        test_content = b\"test content for md5\"\n        local_md5 = hashlib.md5(test_content).hexdigest()\n        \n        # Mock the shell command to return toybox md5sum output\n        # Format: \"md5hash  filename\"\n        mock_dev.shell.return_value = f\"{local_md5}  /data/local/tmp/u2.jar\"\n        \n        # Mock the file read to return our test content\n        with patch(\"builtins.open\", mock_open(read_data=test_content)):\n            result = server._check_device_file_hash(\"test.jar\", \"/data/local/tmp/u2.jar\")\n        \n        # Verify the result is True (hash matches)\n        assert result is True\n        # Verify toybox md5sum was called\n        mock_dev.shell.assert_called_once_with([\"toybox\", \"md5sum\", \"/data/local/tmp/u2.jar\"])\n    \n    def test_toybox_not_found_fallback_to_md5(self, mock_server):\n        \"\"\"Test fallback to md5 command when toybox is not found\"\"\"\n        server, mock_dev = mock_server\n        \n        # Create a temporary file with known content\n        test_content = b\"test content for md5\"\n        local_md5 = hashlib.md5(test_content).hexdigest()\n        \n        # Mock the shell command to return different outputs\n        # First call: toybox not found\n        # Second call: md5 command output (format: \"MD5 (filename) = md5hash\")\n        mock_dev.shell.side_effect = [\n            \"toybox: not found\",\n            f\"MD5 (/data/local/tmp/u2.jar) = {local_md5}\"\n        ]\n        \n        # Mock the file read to return our test content\n        with patch(\"builtins.open\", mock_open(read_data=test_content)):\n            result = server._check_device_file_hash(\"test.jar\", \"/data/local/tmp/u2.jar\")\n        \n        # Verify the result is True (hash matches)\n        assert result is True\n        # Verify both commands were called\n        assert mock_dev.shell.call_count == 2\n        assert mock_dev.shell.call_args_list[0][0][0] == [\"toybox\", \"md5sum\", \"/data/local/tmp/u2.jar\"]\n        assert mock_dev.shell.call_args_list[1][0][0] == [\"md5\", \"/data/local/tmp/u2.jar\"]\n    \n    def test_hash_mismatch(self, mock_server):\n        \"\"\"Test when the hash doesn't match\"\"\"\n        server, mock_dev = mock_server\n        \n        # Create a temporary file with known content\n        test_content = b\"test content for md5\"\n        different_md5 = hashlib.md5(b\"different content\").hexdigest()\n        \n        # Mock the shell command to return a different hash\n        mock_dev.shell.return_value = f\"{different_md5}  /data/local/tmp/u2.jar\"\n        \n        # Mock the file read to return our test content\n        with patch(\"builtins.open\", mock_open(read_data=test_content)):\n            result = server._check_device_file_hash(\"test.jar\", \"/data/local/tmp/u2.jar\")\n        \n        # Verify the result is False (hash doesn't match)\n        assert result is False\n    \n    def test_md5_command_also_fails(self, mock_server):\n        \"\"\"Test when both toybox and md5 commands fail to find the file\"\"\"\n        server, mock_dev = mock_server\n        \n        # Create a temporary file with known content\n        test_content = b\"test content for md5\"\n        \n        # Mock the shell command to return errors for both commands\n        mock_dev.shell.side_effect = [\n            \"toybox: not found\",\n            \"md5: /data/local/tmp/u2.jar: No such file or directory\"\n        ]\n        \n        # Mock the file read to return our test content\n        with patch(\"builtins.open\", mock_open(read_data=test_content)):\n            result = server._check_device_file_hash(\"test.jar\", \"/data/local/tmp/u2.jar\")\n        \n        # Verify the result is False (file not found on device)\n        assert result is False\n"
  },
  {
    "path": "tests/test_import.py",
    "content": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\n\n\"\"\"Created on Wed Mar 20 2024 14:51:03 by codeskyblue\n\"\"\"\n\nimport uiautomator2 as u2\n\n\ndef test_import():\n    u2.Device\n    u2.connect\n    u2.connect_usb\n    u2.Device.app_install\n    u2.Device.app_uninstall\n    u2.Device.app_current\n    u2.Device.app_list\n    u2.Device.shell\n    u2.Device.send_keys\n    u2.Device.click\n    u2.Device.swipe\n    u2.Device.dump_hierarchy\n    u2.Device.freeze_rotation\n    u2.Device.open_notification\n    u2.Device.info\n    u2.Device.xpath\n    u2.Device.clipboard\n    u2.Device.orientation"
  },
  {
    "path": "tests/test_input.py",
    "content": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\n\n\"\"\"Tests for input method functionality\"\"\"\n\nfrom unittest.mock import Mock, patch\n\nimport pytest\n\nfrom uiautomator2._input import InputMethodMixIn\nfrom uiautomator2.exceptions import AdbBroadcastError\n\n\nclass MockInputMethodMixIn(InputMethodMixIn):\n    \"\"\"Mock implementation for testing\"\"\"\n    \n    def __init__(self):\n        self._adb_device = Mock()\n        self._jsonrpc = Mock()\n        self._broadcast_calls = []\n        self._shell_calls = []\n        self._current_ime = 'com.github.uiautomator/.AdbKeyboard'\n    \n    @property\n    def adb_device(self):\n        return self._adb_device\n    \n    @property \n    def jsonrpc(self):\n        return self._jsonrpc\n    \n    def shell(self, args):\n        \"\"\"Mock shell method\"\"\"\n        self._shell_calls.append(args)\n        result = Mock()\n        result.output = self._current_ime\n        return result\n    \n    def _broadcast(self, action, extras=None):\n        \"\"\"Mock broadcast method\"\"\"\n        from uiautomator2._input import BORADCAST_RESULT_OK, BroadcastResult\n        self._broadcast_calls.append((action, extras or {}))\n        return BroadcastResult(BORADCAST_RESULT_OK, \"success\")\n    \n    def __call__(self, **kwargs):\n        \"\"\"Mock selector call for fallback\"\"\"\n        if not hasattr(self, '_mock_element'):\n            self._mock_element = Mock()\n            self._mock_element.set_text = Mock(return_value=True)\n        return self._mock_element\n\n\ndef test_send_keys_hides_keyboard_when_using_custom_ime():\n    \"\"\"Test that send_keys hides keyboard after successful input with custom IME\"\"\"\n    mock_input = MockInputMethodMixIn()\n    \n    # Test successful send_keys with custom IME\n    result = mock_input.send_keys(\"hello world\")\n    \n    # Should return True for successful operation\n    assert result is True\n    \n    # Check broadcast calls - should have both input and hide calls\n    broadcast_calls = mock_input._broadcast_calls\n    assert len(broadcast_calls) >= 2\n    \n    # First call should be for input\n    assert broadcast_calls[0][0] == \"ADB_KEYBOARD_INPUT_TEXT\"\n    assert \"text\" in broadcast_calls[0][1]\n    \n    # Last call should be for hiding keyboard\n    assert broadcast_calls[-1][0] == \"ADB_KEYBOARD_HIDE\"\n    assert broadcast_calls[-1][1] == {}\n\n\ndef test_send_keys_fallback_does_not_hide_keyboard():\n    \"\"\"Test that send_keys fallback to set_text does not hide keyboard\"\"\"\n    mock_input = MockInputMethodMixIn()\n    \n    # Mock the _must_broadcast to raise AdbBroadcastError for input text\n    def failing_must_broadcast(action, extras=None):\n        if action == \"ADB_KEYBOARD_INPUT_TEXT\":\n            raise AdbBroadcastError(\"Simulated failure for input text\")\n        # Should not reach here (keyboard hide) in fallback mode\n        raise AdbBroadcastError(f\"Unexpected broadcast call: {action}\")\n    \n    mock_input._must_broadcast = failing_must_broadcast\n    \n    # Test fallback behavior\n    with patch('warnings.warn'):  # Suppress warning output\n        result = mock_input.send_keys(\"hello world\")\n    \n    # Should return the result from set_text (True in our mock)\n    assert result is True\n    \n    # The element's set_text should have been called\n    mock_element = mock_input(focused=True)\n    assert mock_element.set_text.called\n    mock_element.set_text.assert_called_with(\"hello world\")\n\n\ndef test_hide_keyboard_method():\n    \"\"\"Test the hide_keyboard method directly\"\"\"\n    mock_input = MockInputMethodMixIn()\n    \n    mock_input.hide_keyboard()\n    \n    # Should have made broadcast call for hiding\n    broadcast_calls = mock_input._broadcast_calls\n    assert len(broadcast_calls) >= 1\n    assert broadcast_calls[-1][0] == \"ADB_KEYBOARD_HIDE\"\n    assert broadcast_calls[-1][1] == {}"
  },
  {
    "path": "tests/test_logger.py",
    "content": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\n\n\"\"\"Created on Thu Apr 04 2024 16:57:34 by codeskyblue\n\"\"\"\n\nimport logging\n\nimport pytest\n\nfrom uiautomator2 import enable_pretty_logging\n\n\ndef test_enable_pretty_logging(caplog: pytest.LogCaptureFixture):\n    logger = logging.getLogger(\"uiautomator2\")\n    \n    logger.info(\"should not be printed\")\n    enable_pretty_logging()\n    logger.info(\"hello\")\n    enable_pretty_logging(logging.INFO)\n    logger.info(\"world\")\n    logger.debug(\"should not be printed\")\n\n    # Use caplog.text to check the entire log output as a single string\n    assert \"hello\" in caplog.text\n    assert \"world\" in caplog.text\n    assert \"should not be printed\" not in caplog.text"
  },
  {
    "path": "tests/test_settings.py",
    "content": "# coding: utf-8\n# author: codeskyblue\n\nimport pytest\n\nfrom uiautomator2 import Settings\n\n\ndef test_settings():\n    settings = Settings(None)\n    settings['wait_timeout'] = 10\n    assert settings['wait_timeout'] == 10\n    \n    with pytest.raises(TypeError):\n        settings['wait_timeout'] = '30'\n    assert settings['wait_timeout'] == 10\n    \n    with pytest.raises(AttributeError):\n        settings['not_exists_key'] = 1"
  },
  {
    "path": "tests/test_utils.py",
    "content": "# coding: utf-8\n#\n\nimport threading\nimport time\n\nimport pytest\nfrom PIL import Image\n\nfrom uiautomator2 import utils\n\n\ndef test_list2cmdline():\n    testdata = [\n        [(\"echo\", \"hello\"), \"echo hello\"],\n        [(\"echo\", \"hello&world\"), \"echo 'hello&world'\"],\n        [(\"What's\", \"your\", \"name?\"), \"\"\"'What'\"'\"'s' your 'name?'\"\"\"],\n        [\"echo hello\", \"echo hello\"],\n    ]\n    for args, expect in testdata:\n        cmdline = utils.list2cmdline(args)\n        assert cmdline == expect, \"Args: %s, Expect: %s, Got: %s\" % (args, expect, cmdline)\n\n\ndef test_inject_call():\n    def foo(a, b, c=2):\n        return a*100+b*10+c\n    \n    ret = utils.inject_call(foo, a=2, b=4)\n    assert ret == 242\n\n    with pytest.raises(TypeError):\n        utils.inject_call(foo, 2)\n\n\ndef test_threadsafe_wrapper():\n    class A:\n        n = 0\n\n        @utils.thread_safe_wrapper\n        def call(self):\n            v = self.n\n            time.sleep(.5)\n            self.n = v + 1\n    \n    a = A()\n    th1 = threading.Thread(name=\"th1\", target=a.call)\n    th2 = threading.Thread(name=\"th2\", target=a.call)\n    th1.start()\n    th2.start()\n    th1.join()\n    th2.join()\n    \n    assert 2 == a.n\n\n\ndef test_is_version_compatiable():\n    assert utils.is_version_compatiable(\"1.0.0\", \"1.0.0\")\n    assert utils.is_version_compatiable(\"1.0.0\", \"1.0.1\")\n    assert utils.is_version_compatiable(\"1.0.0\", \"1.2.0\")\n    assert utils.is_version_compatiable(\"1.0.1\", \"1.1.0\")\n\n    assert not utils.is_version_compatiable(\"1.0.1\", \"2.1.0\")\n    assert not utils.is_version_compatiable(\"1.3.1\", \"1.3.0\")\n    assert not utils.is_version_compatiable(\"1.3.1\", \"1.2.0\")\n    assert not utils.is_version_compatiable(\"1.3.1\", \"1.2.2\")\n\n\ndef test_naturalsize():\n    assert utils.natualsize(1) == \"0.0 KB\"\n    assert utils.natualsize(1024) == \"1.0 KB\"\n    assert utils.natualsize(1<<20) == \"1.0 MB\"\n    assert utils.natualsize(1<<30) == \"1.0 GB\"\n\n\ndef test_image_convert():\n    im = Image.new(\"RGB\", (100, 100))\n    im2 = utils.image_convert(im, \"pillow\")\n    assert isinstance(im2, Image.Image)\n    \n    with pytest.raises(ValueError):\n        utils.image_convert(im, \"unknown\")\n\n\ndef test_depreacated():\n    @utils.deprecated(\"use bar instead\")\n    def foo():\n        pass\n\n    with pytest.warns(DeprecationWarning):\n        foo()\n\n\ndef test_with_package_resource():\n    with utils.with_package_resource(\"assets/sync.sh\") as asset_path:\n        assert asset_path.exists()\n        assert asset_path.is_file()\n        assert asset_path.name == \"sync.sh\"\n    \n    # Test that the context manager works properly\n    with pytest.raises(FileNotFoundError):\n        with utils.with_package_resource(\"nonexistent_file.xyz\") as _:\n            pass"
  },
  {
    "path": "tests/test_xpath.py",
    "content": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\n\n\"\"\"Created on Thu Apr 04 2024 16:41:25 by codeskyblue\n\"\"\"\n\nfrom unittest.mock import Mock\n\nimport pytest\nfrom PIL import Image\n\nfrom uiautomator2.xpath import XMLElement, XPath, XPathElementNotFoundError, XPathEntry, XPathSelector, \\\n    convert_to_camel_case, is_xpath_syntax_ok, safe_xmlstr, str2bytes, strict_xpath\n\nmock = Mock()\nmock.screenshot.return_value = Image.new(\"RGB\", (1080, 1920), \"white\")\nmock.dump_hierarchy.return_value = \"\"\"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<hierarchy rotation=\"0\">\n  <node index=\"0\" text=\"\" resource-id=\"android:id/content\" class=\"FrameLayout\" content-desc=\"\" bounds=\"[0,0][1080,1920]\">\n    <node index=\"0\" text=\"n1\" resource-id=\"android:id/text1\" class=\"TextView\" content-desc=\"\" bounds=\"[0,0][1080,100]\" />\n    <node index=\"1\" text=\"n2\" resource-id=\"android:id/text2\" class=\"TextView\" content-desc=\"\" bounds=\"[0,100][1080,200]\" />\n  </node>\n  <node index=\"1\" text=\"\" resource-id=\"android:id/statusBarBackground\" class=\"android.view.View\" package=\"com.android.systemui\" content-desc=\"\" bounds=\"[0,0][1080,24]\" />\n</hierarchy>\n\"\"\"\n\nx = XPathEntry(mock)\n\n\ndef test_safe_xmlstr():\n    for input, expect in [\n        ('android.widget.TextView', 'android.widget.TextView'),\n        ('test$123', 'test.123'),\n        ('$@#&123.456$', '123.456'),\n    ]:\n        assert safe_xmlstr(input) == expect\n\n\ndef test_str2bytes():\n    assert str2bytes(b'123') == b'123'\n    assert str2bytes('123') == b'123'\n\n\ndef test_is_xpath_syntax_ok():\n    assert is_xpath_syntax_ok(\"/a\")\n    assert is_xpath_syntax_ok(\"//a\")\n    assert is_xpath_syntax_ok(\"//a[@text='b]\") is False\n    assert is_xpath_syntax_ok(\"//a[\") is False\n\n\ndef test_convert_to_camel_case():\n    assert convert_to_camel_case(\"hello-world\") == \"helloWorld\"\n\n\ndef test_strict_xpath():\n    for (input, expect) in [\n        (\"@n1\", \"//*[@resource-id='n1']\"),\n        (\"//TextView\", \"//TextView\"),\n        (\"//TextView[@text='n1']\", \"//TextView[@text='n1']\"),\n        (\"(//TextView)[2]\", \"(//TextView)[2]\"),\n        (\"//TextView/\", \"//TextView\"), # test rstrip /\n    ]:\n        assert strict_xpath(input) == expect\n\n\ndef test_XPath():\n    xp = XPath(\"//TextView\")\n    assert xp == \"//TextView\"\n    assert xp.joinpath(\"/n1\") == \"//TextView/n1\"\n\n\n\ndef test_xpath_selector():\n    assert isinstance(x(\"n1\"), XPathSelector)\n    assert isinstance(x(\"//TextView\"), XPathSelector)\n    xp1 = x(\"n1\")\n    xp2 = xp1.child(\"n2\")\n    # xp1 should not be changed\n    assert xp1.get(timeout=0).text == \"n1\"\n    assert xp1.get_text() == \"n1\"\n\n    # match return None or XMLElement\n    assert xp1.match() is not None\n    assert xp2.match() is None\n\n\ndef test_xpath_with_instance():\n    # issue: https://github.com/openatx/uiautomator2/issues/941\n    el = x('(//TextView)[2]').get(0)\n    assert el.text == \"n2\"\n\n\ndef test_xpath_click():\n    x(\"n1\").click()\n    assert mock.click.called\n    assert mock.click.call_args[0] == (540, 50)\n\n    mock.click.reset_mock()\n    assert x(\"n1\").click_exists() == True\n    assert mock.click.call_args[0] == (540, 50)\n    \n    mock.click.reset_mock()\n    assert x(\"n3\").click_exists(timeout=.1) == False\n    assert not mock.click.called\n\n\ndef test_xpath_exists():\n    assert x(\"n1\").exists\n    assert not x(\"n3\").exists\n\n\ndef test_xpath_wait_and_wait_gone():\n    assert x(\"n1\").wait() is True\n    assert x(\"n3\").wait(timeout=.1) is False\n\n    assert x(\"n3\").wait_gone(timeout=.1) is True\n    assert x(\"n1\").wait_gone(timeout=.1) is False\n\n\ndef test_xpath_get():\n    assert x(\"n1\").get().text == \"n1\"\n    assert x(\"n2\").get().text == \"n2\"\n\n    with pytest.raises(XPathElementNotFoundError):\n        x(\"n3\").get(timeout=.1)\n\n\ndef test_xpath_all():\n    assert len(x(\"//TextView\").all()) == 2\n    assert len(x(\"n3\").all()) == 0\n\n    assert len(x(\"n1\").all()) == 1\n    el = x(\"n1\").all()[0]\n    assert isinstance(el, XMLElement)\n    assert el.text == \"n1\"\n\n\ndef test_xpath_element():\n    el = x(\"n1\").get(timeout=0)\n    assert el.text == \"n1\"\n    assert el.center() == (540, 50)\n    assert el.offset(0, 0) == (0, 0)\n    assert el.offset(1, 1) == (1080, 100)\n    assert el.screenshot().size == (1080, 100)\n    assert el.bounds == (0, 0, 1080, 100)\n    assert el.rect == (0, 0, 1080, 100)\n    assert isinstance(el.info, dict)\n    assert el.get_xpath(strip_index=True) == \"/hierarchy/FrameLayout/TextView\"\n    \n    mock.click.reset_mock()\n    el.click()\n    assert mock.click.called\n    assert mock.click.call_args[0] == (540, 50)\n\n    mock.long_click.reset_mock()\n    el.long_click()\n    assert mock.long_click.called\n    assert mock.long_click.call_args[0] == (540, 50)\n\n    mock.swipe.reset_mock()\n    el.swipe(\"up\")\n    assert mock.swipe.called\n\n\n"
  },
  {
    "path": "uiautomator2/__init__.py",
    "content": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n\nfrom __future__ import absolute_import, print_function\n\nimport base64\nimport contextlib\nimport dataclasses\nimport io\nimport logging\nimport os\nimport re\nimport time\nimport warnings\nfrom functools import cached_property\nfrom typing import Any, Dict, List, Optional, Tuple, Union\n\nimport adbutils\nfrom lxml import etree\nfrom PIL import Image\nfrom retry import retry\n\nfrom uiautomator2 import xpath\nfrom uiautomator2._input import InputMethodMixIn\nfrom uiautomator2._proto import HTTP_TIMEOUT, SCROLL_STEPS, Direction\nfrom uiautomator2._selector import Selector, UiObject\nfrom uiautomator2.abstract import AbstractShell, AbstractUiautomatorServer, ShellResponse\nfrom uiautomator2.base import _BaseClient\nfrom uiautomator2.exceptions import *\nfrom uiautomator2.settings import Settings\nfrom uiautomator2.swipe import SwipeExt\nfrom uiautomator2.utils import deprecated, image_convert, list2cmdline\nfrom uiautomator2.watcher import WatchContext, Watcher\n\nWAIT_FOR_DEVICE_TIMEOUT = int(os.getenv(\"WAIT_FOR_DEVICE_TIMEOUT\", 20))\n\nlogger = logging.getLogger(__name__)\n\ndef enable_pretty_logging(level=logging.DEBUG):\n    if not logger.handlers: # pragma: no cover\n        # Configure handler\n        handler = logging.StreamHandler()\n        formatter = logging.Formatter('[%(levelname)1.1s %(asctime)s %(module)s:%(lineno)d pid:%(process)d] %(message)s')\n        handler.setFormatter(formatter)\n        logger.addHandler(handler)\n\n    logger.setLevel(level)\n\nclass _Device(_BaseClient):\n    __orientation = (  # device orientation\n        (0, \"natural\", \"n\", 0), (1, \"left\", \"l\", 90),\n        (2, \"upsidedown\", \"u\", 180), (3, \"right\", \"r\", 270))\n\n    def show_touch_trace(self, pointer_location: bool = True, show_touches: bool = True):\n        \"\"\"\n        Show touch trace on device screen\n\n        Args:\n            pointer_location (bool): screen overlay showing current touch data\n            show_touches (bool): show visual feedback for taps\n        \"\"\"\n        self.shell(f\"settings put system pointer_location {int(pointer_location)}\")\n        self.shell(f\"settings put system show_touches {int(show_touches)}\")\n\n    def window_size(self):\n        \"\"\" return (width, height) \"\"\"\n        w, h = self._dev.window_size()\n        return w, h\n\n    def screenshot(self, filename: Optional[str] = None, format=\"pillow\", display_id: Optional[int] = None):\n        \"\"\"\n        Take screenshot of device\n\n        Returns:\n            PIL.Image.Image, np.ndarray (OpenCV format) or None\n\n        Args:\n            filename (str): saved filename, if filename is set then return None\n            format (str): used when filename is empty. one of [\"pillow\", \"opencv\"]\n            display_id (int): use specific display if device has multiple screen\n\n        Examples:\n            screenshot(\"saved.jpg\")\n            screenshot().save(\"saved.png\")\n            cv2.imwrite('saved.jpg', screenshot(format='opencv'))\n        \"\"\"\n        if display_id is None:\n            base64_data = self.jsonrpc.takeScreenshot(1, 80)\n            # takeScreenshot may return None\n            if base64_data:\n                jpg_raw = base64.b64decode(base64_data)\n                pil_img = Image.open(io.BytesIO(jpg_raw))\n            else:\n                pil_img = self._dev.screenshot(display_id=0)\n        else:\n            pil_img = self._dev.screenshot(display_id=display_id)\n        \n        if filename:\n            pil_img.save(filename)\n            return\n        return image_convert(pil_img, format)\n        \n    def dump_hierarchy(self, compressed=False, pretty=False, max_depth: Optional[int] = None) -> str:\n        \"\"\"\n        Dump window hierarchy\n\n        Args:\n            compressed (bool): return compressed xml\n            pretty (bool): pretty print xml\n            max_depth (int): max depth of hierarchy\n\n        Returns:\n            xml content\n        \"\"\"\n        try:\n            if max_depth is None:\n                max_depth = self.settings['max_depth']\n            content = self._do_dump_hierarchy(compressed, max_depth)\n        except HierarchyEmptyError: # pragma: no cover\n            logger.warning(\"dump empty, return empty xml\")\n            content = '<?xml version=\\'1.0\\' encoding=\\'UTF-8\\' standalone=\\'yes\\' ?>\\r\\n<hierarchy rotation=\"0\" />'\n        \n        if pretty:\n            root = etree.fromstring(content.encode(\"utf-8\"))\n            content = etree.tostring(root, pretty_print=True, encoding='UTF-8', xml_declaration=True)\n            content = content.decode(\"utf-8\")\n        return content\n\n    @retry(HierarchyEmptyError, tries=3, delay=1)\n    def _do_dump_hierarchy(self, compressed=False, max_depth=None) -> str:\n        if max_depth is None:\n            max_depth = 50\n        content = self.jsonrpc.dumpWindowHierarchy(compressed, max_depth)\n        if content == \"\":\n            raise HierarchyEmptyError(\"dump hierarchy is empty\")\n        \n        # '<?xml version=\\'1.0\\' encoding=\\'UTF-8\\' standalone=\\'yes\\' ?>\\r\\n<hierarchy rotation=\"0\" />'\n        if '<hierarchy rotation=\"0\" />' in content:\n            logger.debug(\"dump empty, call clear_traversed_text and retry\")\n            # self.clear_traversed_text()\n            raise HierarchyEmptyError(\"dump hierarchy is empty with no children\")\n        return content\n\n    def implicitly_wait(self, seconds: Optional[float] = None) -> float:\n        \"\"\"set default wait timeout\n        Args:\n            seconds(float): to wait element show up\n\n        Returns:\n            Current implicitly wait seconds\n\n        Deprecated:\n            recommend use: d.settings['wait_timeout'] = 10\n        \"\"\"\n        if seconds:\n            self.settings[\"wait_timeout\"] = seconds\n        return self.settings['wait_timeout']\n\n    @property\n    def pos_rel2abs(self):\n        \"\"\"\n        returns a function which can convert percent size to pixel size\n        \"\"\"\n        size = []\n\n        def _convert(x, y):\n            assert x >= 0\n            assert y >= 0\n\n            if (x < 1 or y < 1) and not size:\n                size.extend(\n                    self.window_size())  # size will be [width, height]\n\n            if x < 1:\n                x = int(size[0] * x)\n            if y < 1:\n                y = int(size[1] * y)\n            return x, y\n\n        return _convert\n\n    @contextlib.contextmanager\n    def _operation_delay(self, operation_name: str = None):\n        before, after = self.settings['operation_delay']\n        # 排除不要求延迟的方法\n        if operation_name not in self.settings['operation_delay_methods']:\n            before, after = 0, 0\n\n        if before:\n            logger.debug(f\"operation [{operation_name}] pre-delay {before}s\")\n            time.sleep(before)\n        yield\n        if after:\n            logger.debug(f\"operation [{operation_name}] post-delay {after}s\")\n            time.sleep(after)\n\n    @property\n    def touch(self):\n        \"\"\"\n        ACTION_DOWN: 0 ACTION_MOVE: 2\n        touch.down(x, y)\n        touch.move(x, y)\n        touch.up(x, y)\n        \"\"\"\n        ACTION_DOWN = 0\n        ACTION_MOVE = 2\n        ACTION_UP = 1\n\n        obj: \"Device\" = self\n\n        class _Touch(object):\n            def down(self, x, y):\n                x, y = obj.pos_rel2abs(x, y)\n                obj.jsonrpc.injectInputEvent(ACTION_DOWN, x, y, 0)\n                return self\n\n            def move(self, x, y):\n                x, y = obj.pos_rel2abs(x, y)\n                obj.jsonrpc.injectInputEvent(ACTION_MOVE, x, y, 0)\n                return self\n\n            def up(self, x, y):\n                \"\"\" ACTION_UP x, y \"\"\"\n                x, y = obj.pos_rel2abs(x, y)\n                obj.jsonrpc.injectInputEvent(ACTION_UP, x, y, 0)\n                return self\n\n            def sleep(self, seconds: float):\n                time.sleep(seconds)\n                return self\n\n        return _Touch()\n\n    def click(self, x: Union[float, int], y: Union[float, int]):\n        x, y = self.pos_rel2abs(x, y)\n        with self._operation_delay(\"click\"):\n            self.jsonrpc.click(x, y)\n\n    def double_click(self, x, y, duration=0.1):\n        \"\"\"\n        double click position\n        \"\"\"\n        x, y = self.pos_rel2abs(x, y)\n        self.touch.down(x, y).up(x, y)\n        time.sleep(duration)\n        self.click(x, y)  # use click last is for htmlreport\n\n    def long_click(self, x, y, duration: float = .5):\n        '''long click at arbitrary coordinates.\n        \n        Args:\n            duration (float): seconds of pressed\n        '''\n        x, y = self.pos_rel2abs(x, y)\n        with self._operation_delay(\"click\"):\n            self.jsonrpc.click(x, y, int(duration*1000))\n\n    def swipe(self, fx, fy, tx, ty, duration: Optional[float] = None, steps: Optional[int] = None):\n        \"\"\"\n        Args:\n            fx, fy: from position\n            tx, ty: to position\n            duration (float): duration\n            steps: 1 steps is about 5ms, if set, duration will be ignore\n\n        Documents:\n            uiautomator use steps instead of duration\n            As the document say: Each step execution is throttled to 5ms per step.\n\n        Links:\n            https://developer.android.com/reference/android/support/test/uiautomator/UiDevice.html#swipe%28int,%20int,%20int,%20int,%20int%29\n        \"\"\"\n        if duration is not None and steps is not None:\n            warnings.warn(\"duration and steps can not be set at the same time, use steps\", UserWarning)\n            duration = None\n        if duration:\n            steps = int(duration * 200)\n        if not steps:\n            steps = SCROLL_STEPS\n        logger.debug(\"swipe from (%s, %s) to (%s, %s), steps: %d\", fx, fy, tx, ty, steps)\n        rel2abs = self.pos_rel2abs\n        fx, fy = rel2abs(fx, fy)\n        tx, ty = rel2abs(tx, ty)\n        steps = max(2, steps)  # step=1 has no swipe effect\n        with self._operation_delay(\"swipe\"):\n            return self.jsonrpc.swipe(fx, fy, tx, ty, steps)\n\n    def swipe_points(self, points: List[Tuple[int, int]], duration: float = 0.5):\n        \"\"\"\n        Args:\n            points: is point array containg at least one point object. eg [[200, 300], [210, 320]]\n            duration: duration to inject between two points\n\n        Links:\n            https://developer.android.com/reference/android/support/test/uiautomator/UiDevice.html#swipe(android.graphics.Point[], int)\n        \"\"\"\n        ppoints = []\n        rel2abs = self.pos_rel2abs\n        for p in points:\n            x, y = rel2abs(p[0], p[1])\n            ppoints.append(x)\n            ppoints.append(y)\n        # Each step execution is throttled to 5ms per step. So for a 100 steps, the swipe will take about 1/ 2 second to complete\n        steps = int(duration / .005)\n        return self.jsonrpc.swipePoints(ppoints, steps)\n\n    def drag(self, sx, sy, ex, ey, duration=0.5):\n        '''Swipe from one point to another point.'''\n        rel2abs = self.pos_rel2abs\n        sx, sy = rel2abs(sx, sy)\n        ex, ey = rel2abs(ex, ey)\n        with self._operation_delay(\"drag\"):\n            return self.jsonrpc.drag(sx, sy, ex, ey, int(duration * 200))\n\n    def press(self, key: Union[int, str], meta=None):\n        \"\"\"\n        press key via name or key code. Supported key name includes:\n            home, back, left, right, up, down, center, menu, search, enter,\n            delete(or del), recent(recent apps), volume_up, volume_down,\n            volume_mute, camera, power.\n        \"\"\"\n        with self._operation_delay(\"press\"):\n            if isinstance(key, int):\n                return self.jsonrpc.pressKeyCode(\n                    key, meta) if meta else self.jsonrpc.pressKeyCode(key)\n            else:\n                return self.jsonrpc.pressKey(key)\n    \n    def long_press(self, key: Union[int, str]):\n        \"\"\"\n        long press key via name or key code\n\n        Args:\n            key: key name or key code\n        \n        Examples:\n            long_press(\"home\") same as \"adb shell input keyevent --longpress KEYCODE_HOME\"\n        \"\"\"\n        with self._operation_delay(\"press\"):\n            if isinstance(key, int):\n                self.shell(\"input keyevent --longpress %d\" % key)\n            else:\n                key = key.upper()\n                self.shell(f\"input keyevent --longpress {key}\")\n\n    def screen_on(self):\n        self.jsonrpc.wakeUp()\n\n    def screen_off(self):\n        self.jsonrpc.sleep()\n\n    @property\n    def orientation(self) -> str:\n        '''\n        orienting the device to left/right or natural.\n        left/l:       rotation=90 , displayRotation=1\n        right/r:      rotation=270, displayRotation=3\n        natural/n:    rotation=0  , displayRotation=0\n        upsidedown/u: rotation=180, displayRotation=2\n        '''\n        return self.__orientation[self.info[\"displayRotation\"]][1]\n\n    @orientation.setter\n    def orientation(self, value: str):\n        '''setter of orientation property.'''\n        for values in self.__orientation:\n            if value in values:\n                # can not set upside-down until api level 18.\n                self.jsonrpc.setOrientation(values[1])\n                break\n        else:\n            raise ValueError(\"Invalid orientation.\")\n\n    def freeze_rotation(self, freezed: bool = True):\n        self.jsonrpc.freezeRotation(freezed)\n\n    @property\n    def last_traversed_text(self):\n        '''get last traversed text. used in webview for highlighted text.'''\n        return self.jsonrpc.getLastTraversedText()\n\n    def clear_traversed_text(self):\n        '''clear the last traversed text.'''\n        self.jsonrpc.clearLastTraversedText()\n    \n    @property\n    def last_toast(self) -> Optional[str]:\n        return self.jsonrpc.getLastToast()\n    \n    def clear_toast(self):\n        self.jsonrpc.clearLastToast()\n\n    def open_notification(self):\n        return self.jsonrpc.openNotification()\n\n    def open_quick_settings(self):\n        return self.jsonrpc.openQuickSettings()\n\n    def open_url(self, url: str):\n        self.shell(\n            ['am', 'start', '-a', 'android.intent.action.VIEW', '-d', url])\n\n    def exists(self, **kwargs):\n        return self(**kwargs).exists\n\n    @property\n    def clipboard(self) -> Optional[str]:\n        return self.jsonrpc.getClipboard()\n\n    @clipboard.setter\n    def clipboard(self, text: str):\n        self.set_clipboard(text)\n\n    def set_clipboard(self, text, label=None):\n        '''\n        Args:\n            text: The actual text in the clip.\n            label: User-visible label for the clip data.\n        '''\n        self.jsonrpc.setClipboard(label, text)\n    \n    def clear_text(self):\n        \"\"\" clear input text \"\"\"\n        self.jsonrpc.clearInputText()\n    \n    def send_keys(self, text: str):\n        \"\"\"\n        send text to focused input area\n        \n        Args:\n            text: input text\n            clear: clear text before input\n        \"\"\"\n        # 使用el =self(focused=True); el.set_text(el.get_text()+text)不可取\n        # 因为placeholder中的文字也会加进去\n        self.clipboard = text\n        if self.clipboard != text:\n            raise UiAutomationError(\"setClipboard failed\")\n        self.jsonrpc.pasteClipboard()\n\n    def keyevent(self, v):\n        \"\"\"\n        Args:\n            v: eg home wakeup back\n        \"\"\"\n        v = v.upper()\n        self.shell(\"input keyevent \" + v)\n\n    @cached_property\n    def serial(self) -> str:\n        \"\"\"\n        If connected with USB, here should return self._serial\n        When this situation happends\n\n            d = u2.connect_usb(\"10.0.0.1:5555\")\n            d.serial # should be \"10.0.0.1:5555\"\n            d.shell(['getprop', 'ro.serialno']).output.strip() # should uniq str like ffee123ca\n\n        This logic should not change, because it used in tmq-service\n        and if you break it, some people will not happy\n        \"\"\"\n        if self._serial:\n            return self._serial\n        return self.shell(['getprop', 'ro.serialno']).output.strip()\n    \n    def __call__(self, **kwargs) -> 'UiObject':\n        return UiObject(self, Selector(**kwargs))\n\n\nclass _AppMixIn(AbstractShell):\n    def session(self, package_name: str, attach: bool = False) -> \"Session\":\n        \"\"\"\n        launch app and keep watching the app's state\n\n        Args:\n            package_name: package name\n            attach: attach to existing session or not\n\n        Returns:\n            Session\n        \"\"\"\n        self.app_start(package_name, stop=not attach)\n        return Session(self.adb_device, package_name)\n\n    def _compat_shell_ps(self) -> str:\n        \"\"\"\n        Compatible with some devices that does not support `ps` command\n        \"\"\"\n        output = self.shell(\"ps -A\").output\n        if len(output.strip().splitlines()) <= 1:\n            output = self.shell(\"ps\").output\n        return output.strip().replace(\"\\r\\n\", \"\\n\")\n        \n    def _pidof_app(self, package_name) -> Optional[int]:\n        \"\"\"\n        Return pid of package name\n        \"\"\"\n        output = self._compat_shell_ps()\n        lines = output.splitlines()\n        for line in lines:\n            # line example: u0_a1    1318  123   1010000 27580 SyS_epoll_ 0000000000 S com.github.uiautomator\n            fields = line.strip().split()\n            if len(fields) < 9:\n                continue\n            if fields[-1] == package_name:\n                return int(fields[1])\n\n    def app_current(self):\n        \"\"\"\n        Returns:\n            dict(package, activity, pid?)\n\n        Raises:\n            DeviceError\n\n        For developer:\n            Function reset_uiautomator need this function, so can't use jsonrpc here.\n        \"\"\"\n        info = self.adb_device.app_current()\n        if info:\n            return dataclasses.asdict(info)\n        raise DeviceError(\"Couldn't get focused app\")\n\n    def app_install(self, data: str):\n        \"\"\"\n        Install app\n\n        Args:\n            data: can be file path or url or file object\n        \"\"\"\n        self.adb_device.install(data)\n\n    def wait_activity(self, activity, timeout=10) -> bool:\n        \"\"\" wait activity\n        Args:\n            activity (str): name of activity\n            timeout (float): max wait time\n\n        Returns:\n            bool of activity\n        \"\"\"\n        deadline = time.time() + timeout\n        while time.time() < deadline:\n            current_activity = self.app_current().get('activity')\n            if activity == current_activity:\n                return True\n            time.sleep(.5)\n        return False\n\n    def app_start(self, package_name: str, activity: Optional[str] = None, wait: bool = False, stop: bool = False, use_monkey: bool = False):\n        \"\"\" Launch application\n        Args:\n            package_name (str): package name\n            activity (str): app activity\n            stop (bool): Stop app before starting the activity. (require activity)\n            use_monkey (bool): use monkey command to start app when activity is not given\n            wait (bool): wait until app started. default False\n        \"\"\"\n        if stop:\n            self.app_stop(package_name)\n\n        if use_monkey or not activity:\n            self.shell([\n                'monkey', '-p', package_name, '-c',\n                'android.intent.category.LAUNCHER', '1'\n            ])\n            if wait:\n                self.app_wait(package_name)\n            return\n\n        # if not activity:\n        #     info = self.app_info(package_name)\n        #     activity = info['mainActivity']\n        #     if activity.find(\".\") == -1:\n        #         activity = \".\" + activity\n\n        # -D: enable debugging\n        # -W: wait for launch to complete\n        # -S: force stop the target app before starting the activity\n        # --user <USER_ID> | current: Specify which user to run as; if not\n        #    specified then run as the current user.\n        # -e <EXTRA_KEY> <EXTRA_STRING_VALUE>\n        # --ei <EXTRA_KEY> <EXTRA_INT_VALUE>\n        # --ez <EXTRA_KEY> <EXTRA_BOOLEAN_VALUE>\n        args = [\n            'am', 'start', '-a', 'android.intent.action.MAIN', '-c',\n            'android.intent.category.LAUNCHER',\n            '-n', f'{package_name}/{activity}'\n        ]\n        self.shell(args)\n\n        if wait:\n            self.app_wait(package_name)\n\n    def app_wait(self,\n                 package_name: str,\n                 timeout: float = 20.0,\n                 front=False) -> int:\n        \"\"\" Wait until app launched\n        Args:\n            package_name (str): package name\n            timeout (float): maxium wait time\n            front (bool): wait until app is current app\n\n        Returns:\n            pid (int) 0 if launch failed\n        \"\"\"\n        pid = None\n        deadline = time.time() + timeout\n        while time.time() < deadline:\n            if front:\n                if self.app_current()['package'] == package_name:\n                    pid = self._pidof_app(package_name)\n            else:\n                if package_name in self.app_list_running():\n                    pid = self._pidof_app(package_name)\n            if pid:\n                return pid\n            time.sleep(1)\n\n        return pid or 0\n\n    def app_list(self, filter: str = None) -> List[str]:\n        \"\"\"\n        List installed app package names\n\n        Args:\n            filter: [-f] [-d] [-e] [-s] [-3] [-i] [-u] [--user USER_ID] [FILTER]\n        \n        Returns:\n            list of apps by filter\n        \"\"\"\n        output, _ = self.shell(['pm', 'list', 'packages', filter])\n        packages = re.findall(r'package:([^\\s]+)', output)\n        return list(packages)\n\n    def app_list_running(self) -> List[str]:\n        \"\"\"\n        Returns:\n            list of running apps\n        \"\"\"\n        output, _ = self.shell('pm list packages')\n        packages = re.findall(r'package:([^\\s]+)', output)\n        ps_output = self._compat_shell_ps()\n        process_names = re.findall(r'(\\S+)$', ps_output, re.M)\n        return list(set(packages).intersection(process_names))\n\n    def app_stop(self, package_name: str):\n        \"\"\" Stop one application \"\"\"\n        self.adb_device.app_stop(package_name)\n\n    def app_stop_all(self, excludes=[]):\n        \"\"\" Stop all third party applications\n        Args:\n            excludes (list): apps that do now want to kill\n\n        Returns:\n            a list of killed apps\n        \"\"\"\n        our_apps = ['com.github.uiautomator', 'com.github.uiautomator.test']\n        kill_pkgs = set(self.app_list_running()).difference(our_apps +\n                                                            excludes)\n        for pkg_name in kill_pkgs:\n            self.app_stop(pkg_name)\n        return list(kill_pkgs)\n\n    def app_clear(self, package_name: str):\n        \"\"\" Stop and clear app data: pm clear \"\"\"\n        self.adb_device.app_clear(package_name)\n\n    def app_uninstall(self, package_name: str) -> bool:\n        \"\"\" Uninstall an app \n\n        Returns:\n            bool: success\n        \"\"\"\n        ret = self.shell([\"pm\", \"uninstall\", package_name])\n        return ret.exit_code == 0\n\n    def app_uninstall_all(self, excludes=[], verbose=False):\n        \"\"\" Uninstall all apps \"\"\"\n        our_apps = ['com.github.uiautomator', 'com.github.uiautomator.test']\n        output, _ = self.shell(['pm', 'list', 'packages', '-3'])\n        pkgs = re.findall(r'package:([^\\s]+)', output)\n        pkgs = set(pkgs).difference(our_apps + excludes)\n        pkgs = list(pkgs)\n        for pkg_name in pkgs:\n            if verbose:\n                print(\"uninstalling\", pkg_name, \" \", end=\"\", flush=True)\n            ok = self.app_uninstall(pkg_name)\n            if verbose:\n                print(\"OK\" if ok else \"FAIL\")\n\n        return pkgs\n\n    def app_info(self, package_name: str) -> Dict[str, Any]:\n        \"\"\"\n        Get app info\n\n        Args:\n            package_name (str): package name\n\n        Return example:\n            {\n                \"versionName\": \"1.1.7\",\n                \"versionCode\": 1001007\n            }\n\n        Raises:\n            AppNotFoundError\n        \"\"\"\n        info = self.adb_device.app_info(package_name)\n        if not info:\n            raise AppNotFoundError(\"App not installed\", package_name)\n        return {\n            \"versionName\": info.version_name,\n            \"versionCode\": info.version_code,\n        }\n\n    def app_auto_grant_permissions(self, package_name: str):\n        \"\"\" auto grant permissions\n\n        Args:\n            package_name (str): package name\n        \n        Help of \"adb shell pm\":\n            grant [--user USER_ID] PACKAGE PERMISSION\n            revoke [--user USER_ID] PACKAGE PERMISSION\n                These commands either grant or revoke permissions to apps.  The permissions\n                must be declared as used in the app's manifest, be runtime permissions\n                (protection level dangerous), and the app targeting SDK greater than Lollipop MR1 (API level 22).\n        \n        Help of \"Android official pm\" see <https://developer.android.com/tools/adb#pm>\n            Grant a permission to an app. On devices running Android 6.0 (API level 23) and higher,\n              the permission can be any permission declared in the app manifest.\n            On devices running Android 5.1 (API level 22) and lower,\n              must be an optional permission defined by the app.\n        \"\"\"\n        sdk_version_output = self.shell(['getprop', 'ro.build.version.sdk']).output.strip()\n        sdk_version = int(sdk_version_output) if sdk_version_output.isdigit() else None\n        if sdk_version is None:\n            logger.warning(\"can't get sdk version\")\n            return\n        if sdk_version < 23:\n            # TODO: support android 5.1 (API 22) and lower\n            logger.warning(\"auto grant permissions only support android 6.0+ (API 23+)\")\n            return\n        \n        dumpsys_package_output = self.shell(['dumpsys', 'package',  package_name]).output\n        target_sdk_match = re.search(r'targetSdk=(\\d+)', dumpsys_package_output)\n        if not target_sdk_match:\n            logger.warning(\"can't get targetSdk from dumpsys package\")\n            return\n        target_sdk = int(target_sdk_match.group(1))\n        if target_sdk < 22:\n            logger.warning(\"auto grant permissions only support app targetSdk >= 22\")\n            return\n            \n        permissions = re.findall(r'(android\\.\\w*\\.?permission\\.\\w+): granted=false', dumpsys_package_output)\n        for permission in permissions:\n            self.shell(['pm', 'grant', package_name, permission])\n            logger.info(f'auto grant permission {permission}')\n\n\nclass _DeprecatedMixIn: # pragma: no cover\n    @property\n    def wait_timeout(self):  # wait element timeout\n        return self.settings['wait_timeout']\n\n    @wait_timeout.setter\n    def wait_timeout(self, v: Union[int, float]):\n        self.settings['wait_timeout'] = v\n\n    @property\n    def click_post_delay(self):\n        \"\"\" Deprecated or not deprecated, this is a question \"\"\"\n        return self.settings['post_delay']\n\n    @click_post_delay.setter\n    def click_post_delay(self, v: Union[int, float]):\n        self.settings['post_delay'] = v\n\n    def unlock(self):\n        \"\"\" unlock screen with swipe from left-bottom to right-top \"\"\"\n        if not self.info['screenOn']:\n            # WAKEUP might be stuck\n            self.shell(\"input keyevent POWER\")\n            self.swipe(0.1, 0.9, 0.9, 0.1)\n\n    def show_float_window(self, show=True):\n        \"\"\" 显示悬浮窗，提高uiautomator运行的稳定性 \"\"\"\n        print(\"show_float_window is deprecated, this is not needed anymore\")\n    \n    @deprecated(reason=\"use d.toast.show(text, duration) instead\")\n    def make_toast(self, text, duration=1.0):\n        \"\"\" Show toast\n        Args:\n            text (str): text to show\n            duration (float): seconds of display\n        \"\"\"\n        return self.jsonrpc.makeToast(text, duration * 1000)\n    \n    @property\n    def toast(self):\n        obj = self\n\n        class Toast(object):\n            def get_message(self,\n                            wait_timeout=10,\n                            cache_timeout=10,\n                            default=None):\n                \"\"\"\n                Args:\n                    wait_timeout: seconds of max wait time if toast now show right now\n                    cache_timeout: depreacated\n                    default: default messsage to return when no toast show up\n\n                Returns:\n                    None or toast message\n                \"\"\"\n                deadline = time.time() + wait_timeout\n                while 1:\n                    message = obj.jsonrpc.getLastToast()\n                    if message:\n                        return message\n                    if time.time() > deadline:\n                        return default\n                    time.sleep(.5)\n\n            def reset(self):\n                return obj.jsonrpc.clearLastToast()\n\n            def show(self, text, duration=1.0):\n                return obj.jsonrpc.makeToast(text, duration * 1000)\n\n        return Toast()\n    \n    def set_orientation(self, value: str):\n        '''setter of orientation property.'''\n        self.orientation = value\n\n\nclass _PluginMixIn:\n    def watch_context(self, autostart: bool = True, builtin: bool = False) -> WatchContext:\n        wc = WatchContext(self, builtin=builtin)\n        if autostart:\n            wc.start()\n        return wc\n\n    @cached_property\n    def watcher(self) -> Watcher:\n        return Watcher(self)\n\n    @cached_property\n    def xpath(self) -> xpath.XPathEntry:\n        return xpath.XPathEntry(self)\n\n    @cached_property\n    def image(self):\n        from uiautomator2 import image as _image\n        return _image.ImageX(self)\n\n    @cached_property\n    def screenrecord(self):\n        from uiautomator2 import screenrecord as _sr\n        return _sr.Screenrecord(self)\n\n    @cached_property\n    def swipe_ext(self) -> SwipeExt:\n        return SwipeExt(self)\n\n\nclass Device(_Device, _AppMixIn, _PluginMixIn, InputMethodMixIn, _DeprecatedMixIn):\n    \"\"\" Device object \"\"\"\n    \n    def clear_text(self):\n        \"\"\" clear input text \"\"\"\n        if self.is_input_ime_installed():\n            InputMethodMixIn.clear_text(self)\n        else:\n            _Device.clear_text(self)\n    \n    def send_keys(self, text: str, clear: bool = False):\n        \"\"\"\n        send text to focused input area\n        \n        Args:\n            text: input text\n            clear: clear text before input\n        \"\"\"\n        if clear:\n            self.clear_text()    \n        if self.is_input_ime_installed():\n            InputMethodMixIn.send_keys(self, text)\n            return\n        try:\n            _Device.send_keys(self, text)            \n        except:\n            # 安装输入法后继续输入\n            InputMethodMixIn.send_keys(self, text)\n\n\nclass Session(Device):\n    \"\"\"Session keeps watch the app status\n    each jsonrpc call will check if the package is still running\n    \"\"\"\n    def __init__(self, dev: adbutils.AdbDevice, package_name: str):\n        super().__init__(dev)\n        self._package_name = package_name\n        self._pid = self.app_wait(self._package_name)\n    \n    def running(self) -> bool:\n        return self._pid == self._pidof_app(self._package_name)\n\n    @property\n    def pid(self) -> int:\n        return self._pid\n        \n    def jsonrpc_call(self, method: str, params: Any = None, timeout: float = 10) -> Any:\n        if not self.running():\n            raise SessionBrokenError(f\"app:{self._package_name} pid:{self._pid} is quit\")\n        return super().jsonrpc_call(method, params, timeout)\n    \n    def restart(self):\n        \"\"\" restart app \"\"\"\n        self.app_start(self._package_name, wait=True, stop=True)\n        self._pid = self._pidof_app(self._package_name)\n    \n    def close(self):\n        \"\"\" close app \"\"\"\n        self.app_stop(self._package_name)\n        self._pid = None\n\n    def __enter__(self):\n        return self\n\n    def __exit__(self, exc_type, exc_val, exc_tb):\n        self.close()\n\n\ndef connect(serial: Union[str, adbutils.AdbDevice] = None) -> Device:\n    \"\"\"\n    Args:\n        serial (str): Android device serialno\n\n    Returns:\n        Device\n\n    Raises:\n        ConnectError\n\n    Example:\n        connect(\"10.0.0.1:5555\")\n        connect(\"cff1123ea\")  # adb device serial number\n    \"\"\"\n    if not serial:\n        serial = os.getenv(\"ANDROID_SERIAL\")\n    return connect_usb(serial)\n\n\ndef connect_usb(serial: Optional[str] = None) -> Device:\n    \"\"\"\n    Args:\n        serial (str): android device serial\n\n    Returns:\n        Device\n\n    Raises:\n        ConnectError\n    \"\"\"\n    if not serial:\n        serial = adbutils.adb.device()\n    return Device(serial)\n"
  },
  {
    "path": "uiautomator2/__main__.py",
    "content": "# coding: utf-8\n#\n\nfrom __future__ import absolute_import, print_function\n\nimport argparse\nimport json\nimport logging\nimport pathlib\nimport shutil\nimport sys\n\nimport adbutils\n\nimport uiautomator2 as u2\nfrom uiautomator2 import enable_pretty_logging\nfrom uiautomator2.utils import with_package_resource\nfrom uiautomator2.version import __version__\n\nlogger = logging.getLogger(__name__)\n\n\ndef cmd_init(args):\n    serial = args.serial or args.serial_optional\n    if serial:\n        d = u2.connect(serial)\n        logger.debug(\"install apk to %s\", d.serial)\n        d._setup_jar()\n    else:\n        for dev in adbutils.adb.iter_device():\n            d = u2.connect(dev)\n            logger.debug(\"install apk to %s\", d.serial)\n            d._setup_jar()\n            d._setup_ime()\n\n\ndef cmd_purge(args):\n    \"\"\"remove minicap, minitouch, uiautomator ...\"\"\"\n    dev = adbutils.adb.device(args.serial)\n    dev.uninstall(\"com.github.uiautomator\")\n    dev.uninstall(\"com.github.uiautomator.test\")\n    dev.shell([\"/data/local/tmp/atx-agent\", \"server\", \"--stop\"])\n    dev.shell([\"rm\", \"/data/local/tmp/atx-agent\"])\n    logger.info(\"atx-agent stopped and removed\")\n    dev.shell([\"rm\", \"/data/local/tmp/minicap\"])\n    dev.shell([\"rm\", \"/data/local/tmp/minicap.so\"])\n    dev.shell([\"rm\", \"/data/local/tmp/minitouch\"])\n    logger.info(\"minicap, minitouch removed\")\n    dev.shell([\"pm\", \"uninstall\", \"com.github.uiautomator\"])\n    dev.shell([\"pm\", \"uninstall\", \"com.github.uiautomator.test\"])\n    logger.info(\"com.github.uiautomator uninstalled, all done !!!\")\n\n\ndef cmd_copy_assets(args):\n    target_dir = pathlib.Path(\"assets\")\n    target_dir.mkdir(exist_ok=True)\n    with with_package_resource(\"assets/u2.jar\") as jar_path:\n        target_path = target_dir / \"u2.jar\"\n        shutil.copy2(jar_path, target_path)\n        print(\"Copied u2.jar to\", target_path)\n    with with_package_resource(\"assets/app-uiautomator.apk\") as apk_path:\n        target_path = target_dir / \"app-uiautomator.apk\"\n        shutil.copy2(apk_path, target_path)\n        print(\"Copied app-uiautomator.apk to\", target_path)\n        \ndef cmd_screenshot(args):\n    d = u2.connect(args.serial)\n    d.screenshot().save(args.filename)\n    print(\"Save screenshot to %s\" % args.filename)\n\n\ndef cmd_install(args):\n    u = u2.connect(args.serial)\n    pkg_name = u.app_install(args.url)\n    print(\"Installed\", pkg_name)\n\n\ndef cmd_uninstall(args):\n    d = u2.connect(args.serial)\n    if args.all:\n        d.app_uninstall_all(verbose=True)\n    else:\n        for package_name in args.package_name:\n            print('Uninstall \"%s\" ' % package_name, end=\"\", flush=True)\n            ok = d.app_uninstall(package_name)\n            print(\"OK\" if ok else \"FAIL\")\n\n\ndef cmd_start(args):\n    d = u2.connect(args.serial)\n    d.app_start(args.package_name)\n\n\ndef cmd_stop(args):\n    d = u2.connect(args.serial)\n    if args.all:\n        d.app_stop_all()\n        return\n\n    for package_name in args.package_name:\n        print('am force-stop \"%s\" ' % package_name)\n        d.app_stop(package_name)\n\n\ndef cmd_current(args):\n    d = u2.connect(args.serial)\n    print(json.dumps(d.app_current(), indent=4), flush=True)\n\n\ndef cmd_doctor(args):\n    \"\"\"check if environment is fine\"\"\"\n    d = u2.connect(args.serial)\n    logger.debug(\"device serial: %s\", d.serial)\n    try:\n        d.info\n        logger.info(\"uiautomator2 is OK\")\n    except Exception as e:\n        logger.error(\"error: %s\", e)\n        sys.exit(1)\n\n\ndef cmd_version(args):\n    \"\"\"print uiautomator2 lib version\"\"\"\n    print(\"uiautomator2 version: %s\" % __version__)\n\n\ndef cmd_console(args):\n    import code\n    import platform\n    \n    d = u2.connect(args.serial)\n    model = d.shell(\"getprop ro.product.model\").output.strip()\n    serial = d.serial\n    try:\n        import IPython\n        from traitlets.config import get_config\n\n        c = get_config()\n        c.InteractiveShellEmbed.colors = \"neutral\"\n        IPython.embed(config=c, header=f\"IPython is ready, uiautomator2: {__version__}, try d.info\")\n    except ImportError:\n        _vars = globals().copy()\n        _vars.update(locals())\n        shell = code.InteractiveConsole(_vars)\n        shell.interact(\n            banner=\"Python: %s\\nDevice: %s(%s)\"\n            % (platform.python_version(), model, serial)\n        )\n\n\n_commands = [\n    dict(action=cmd_version, command=\"version\", help=\"show version\"),\n    dict(\n        action=cmd_init,\n        command=\"init\",\n        help=\"install enssential resources to device\",\n        flags=[\n            dict(\n                args=[\"--addr\"],\n                default=\"127.0.0.1:7912\",\n                help=\"atx-agent listen address\",\n            ),\n            dict(args=[\"--serial\", \"-s\"], type=str, help=\"serial number\"),\n            dict(\n                args=[\"serial_optional\"],\n                nargs=\"?\",\n                help=\"serial number, same as --serial\",\n            ),\n        ],\n    ),\n    dict(\n        action=cmd_copy_assets,\n        command=\"copy-assets\",\n        help=\"copy uiautomator2 assets to current directory\",\n    ),\n    dict(\n        action=cmd_screenshot,\n        command=\"screenshot\",\n        help=\"take device screenshot\",\n        flags=[\n            dict(\n                args=[\"filename\"],\n                nargs=\"?\",\n                default=\"screenshot.jpg\",\n                type=str,\n                help=\"output filename, jpg or png\",\n            )\n        ],\n    ),\n    dict(\n        action=cmd_install,\n        command=\"install\",\n        help=\"install packages\",\n        flags=[\n            dict(args=[\"url\"], help=\"package url\"),\n        ],\n    ),\n    dict(\n        action=cmd_uninstall,\n        command=\"uninstall\",\n        help=\"uninstall packages\",\n        flags=[\n            dict(args=[\"--all\"], action=\"store_true\", help=\"uninstall all packages\"),\n            dict(args=[\"package_name\"], nargs=\"*\", help=\"package name\"),\n        ],\n    ),\n    dict(\n        action=cmd_start,\n        command=\"start\",\n        help=\"start application\",\n        flags=[dict(args=[\"package_name\"], type=str, nargs=None, help=\"package name\")],\n    ),\n    dict(\n        action=cmd_stop,\n        command=\"stop\",\n        help=\"stop application\",\n        flags=[\n            dict(args=[\"--all\"], action=\"store_true\", help=\"stop all\"),\n            dict(args=[\"package_name\"], nargs=\"*\", help=\"package name\"),\n        ],\n    ),\n    dict(action=cmd_current, command=\"current\", help=\"show current application\"),\n    dict(action=cmd_doctor, command=\"doctor\", help=\"detect connect problem\"),\n    dict(\n        action=cmd_console, command=\"console\", help=\"launch interactive python console\"\n    ),\n    dict(\n        action=cmd_purge,\n        command=\"purge\",\n        help=\"remove minitouch, minicap, atx app etc, from device\",\n    ),\n]\n\n\ndef main():\n    # yapf: disable\n    parser = argparse.ArgumentParser(\n        formatter_class=argparse.ArgumentDefaultsHelpFormatter)\n    parser.add_argument(\"-d\", \"--debug\", action=\"store_true\",\n                        help=\"show log\")\n    parser.add_argument('-s', '--serial', type=str,\n                        help='device serial number')\n\n    subparser = parser.add_subparsers(dest='subparser')\n\n    actions = {}\n    for c in _commands:\n        cmd_name = c['command']\n        actions[cmd_name] = c['action']\n        sp = subparser.add_parser(cmd_name, help=c.get('help'),\n                                  formatter_class=argparse.ArgumentDefaultsHelpFormatter)\n        for f in c.get('flags', []):\n            args = f.get('args')\n            if not args:\n                args = ['-'*min(2, len(n)) + n for n in f['name']]\n            kwargs = f.copy()\n            kwargs.pop('name', None)\n            kwargs.pop('args', None)\n            sp.add_argument(*args, **kwargs)\n\n    args = parser.parse_args()\n    enable_pretty_logging()\n\n    if args.debug:\n        logger.debug(\"args: %s\", args)\n\n    if args.subparser:\n        actions[args.subparser](args)\n        return\n\n    parser.print_help()\n    # yapf: enable\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "uiautomator2/_input.py",
    "content": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\n\n\"\"\"Created on Wed May 22 2024 16:23:56 by codeskyblue\n\"\"\"\n\nimport base64\nimport logging\nimport re\nimport time\nimport warnings\nfrom dataclasses import dataclass\nfrom pathlib import Path\nfrom typing import Dict, List, Optional, Union\n\nimport adbutils\nfrom retry import retry\n\nfrom uiautomator2.abstract import AbstractShell\nfrom uiautomator2.exceptions import AdbBroadcastError, DeviceError, InputIMEError\nfrom uiautomator2.utils import deprecated, with_package_resource\n\nlogger = logging.getLogger(__name__)\n\n@dataclass\nclass BroadcastResult:\n    code: Optional[int]\n    data: Optional[str]\n\n\nBORADCAST_RESULT_OK = -1\nBROADCAST_RESULT_CANCELED = 0\n\n\n\nclass InputMethodMixIn(AbstractShell):\n    # @property\n    # def clipboard(self) -> Optional[str]:\n    #     result = self._broadcast(\"ADB_KEYBOARD_GET_CLIPBOARD\")\n    #     if result.code == BORADCAST_RESULT_OK:\n    #         return base64.b64decode(result.data).decode('utf-8')\n    #     # jsonrpc.getClipboard is not OK for now\n    #     return None\n    \n    @property\n    def __ime_id(self) -> str:\n        return 'com.github.uiautomator/.AdbKeyboard'\n\n    def set_input_ime(self, enable: bool = True):\n        \"\"\" Enable of Disable InputIME \"\"\"\n        if not enable:\n            self.shell(['ime', 'disable', self.__ime_id])\n            return\n        if self.current_ime() == self.__ime_id:\n            return\n        # prepare ime\n        if self.__ime_id not in self.__get_ime_list():\n            self._setup_ime()\n        assert self.__ime_id in self.__get_ime_list()\n        \n        self.shell(['ime', 'enable', self.__ime_id])\n        self.shell(['ime', 'set', self.__ime_id])\n        self.shell(['settings', 'put', 'secure', 'default_input_method', self.__ime_id])\n        self._wait_ime_ready()\n    \n    def is_input_ime_installed(self) -> bool:\n        return self.__ime_id in self.__get_ime_list()\n        \n    def _setup_ime(self):\n        logger.debug(\"installing AdbKeyboard ime\")\n        with with_package_resource(\"assets/app-uiautomator.apk\") as ime_apk_path:\n            try:\n                self.adb_device.install(str(ime_apk_path), nolaunch=True, uninstall=True)\n            except adbutils.AdbError as e:\n                self.adb_device.uninstall(self.__ime_id.split('/')[0])\n                self.adb_device.install(str(ime_apk_path), nolaunch=True, uninstall=True)\n            \n        # wait for ime registered\n        for _ in range(10):\n            if self.__ime_id in self.__get_ime_list():\n                return\n            time.sleep(.3)\n        raise InputIMEError(\"install AdbKeyboard ime failed\")\n    \n    def _broadcast(self, action: str, extras: Dict[str, str] = {}) -> BroadcastResult:\n        # requires ATX 2.4.0+\n        args = ['am', 'broadcast', '-a', action]\n        for k, v in extras.items():\n            if isinstance(v, int):\n                args.extend(['--ei', k, str(v)])\n            else:\n                args.extend(['--es', k, v])\n        # Example output: result=-1 data=\"success\"\n        output = self.shell(args).output\n        m_result = re.search(r'result=(-?\\d+)', output)\n        m_data = re.search(r'data=\"([^\"]+)\"', output)\n        result = int(m_result.group(1)) if m_result else None\n        data = m_data.group(1) if m_data else None\n        return BroadcastResult(result, data)\n    \n    @retry(AdbBroadcastError, tries=3, delay=1, jitter=0.5)\n    def _must_broadcast(self, action: str, extras: Dict[str, str] = {}):\n        result = self._broadcast(action, extras)\n        if result.code != BORADCAST_RESULT_OK:\n            raise AdbBroadcastError(f\"broadcast {action} failed: {result.data}\")\n\n    def send_keys(self, text: str):\n        try:\n            self.set_input_ime()\n            btext = text.encode('utf-8')\n            base64text = base64.b64encode(btext).decode()\n            cmd = \"ADB_KEYBOARD_INPUT_TEXT\"\n            self._must_broadcast(cmd, {\"text\": base64text})\n            # Hide keyboard after successful input when using custom IME\n            self._must_broadcast('ADB_KEYBOARD_HIDE')\n            return True\n        except AdbBroadcastError:\n            warnings.warn(\n                \"set FastInputIME failed. use \\\"d(focused=True).set_text instead\\\"\",\n                Warning)\n            return self(focused=True).set_text(text)\n        \n    def send_action(self, code: Union[str, int] = None):\n        \"\"\"\n        Simulate input method edito code\n\n        Args:\n            code (str or int): input method editor code\n\n        Examples:\n            send_action(\"search\"), send_action(3)\n\n        Refs:\n            https://developer.android.com/reference/android/view/inputmethod/EditorInfo\n        \"\"\"\n        self.set_input_ime(True)\n        __alias = {\n            \"go\": 2,\n            \"search\": 3,\n            \"send\": 4,\n            \"next\": 5,\n            \"done\": 6,\n            \"previous\": 7,\n        }\n        if isinstance(code, str):\n            code = __alias.get(code, code)\n        if code:\n            self._must_broadcast('ADB_KEYBOARD_EDITOR_CODE', {\"code\": str(code)})\n        else:\n            self._must_broadcast('ADB_KEYBOARD_SMART_ENTER')\n\n    def clear_text(self):\n        self.set_input_ime(True)\n        self._must_broadcast('ADB_KEYBOARD_CLEAR_TEXT')\n\n    def current_ime(self) -> str:\n        \"\"\" Current input method\n        Returns:\n            ime_method\n\n        Example output:\n            \"com.github.uiautomator/.FastInputIME\"\n        \"\"\"\n        return self.shell(['settings', 'get', 'secure', 'default_input_method']).output.strip()\n        # _INPUT_METHOD_RE = re.compile(r'mCurMethodId=([-_./\\w]+)')\n        # dim, _ = self.shell(['dumpsys', 'input_method'])\n        # m = _INPUT_METHOD_RE.search(dim)\n        # method_id = None if not m else m.group(1)\n        # shown = \"mInputShown=true\" in dim\n        # return (method_id, shown)\n    \n    def _wait_ime_ready(self, timeout: float = 5.0) -> bool:\n        \"\"\" Wait for input method is ready \"\"\"\n        deadline = time.time() + timeout\n        while time.time() < deadline:\n            if self.current_ime() == self.__ime_id:\n                return True\n            time.sleep(0.1)\n        return False\n    \n    def __get_ime_list(self) -> List[str]:\n        ret = self.shell(['ime', 'list', '-s', '-a'])\n        return ret.output.strip().splitlines(keepends=False)\n\n    def hide_keyboard(self):\n        \"\"\" Hide keyboard \"\"\"\n        self.set_input_ime()\n        self._must_broadcast('ADB_KEYBOARD_HIDE')\n        \n    @deprecated(reason=\"use set_input_ime instead\")\n    def set_fastinput_ime(self, enable: bool = True):\n        return self.set_input_ime(enable)\n    \n    @deprecated(reason=\"use set_input_ime instead\")\n    def wait_fastinput_ime(self, timeout=5.0):\n        \"\"\" wait FastInputIME is ready (Depreacated in version 3.1) \"\"\"\n        pass\n"
  },
  {
    "path": "uiautomator2/_proto.py",
    "content": "import enum\n\nSCROLL_STEPS = 55\nHTTP_TIMEOUT = 300\n\nclass Direction(str, enum.Enum):\n    LEFT = \"left\"\n    RIGHT = \"right\"\n    UP = \"up\"\n    DOWN = \"down\"\n\n    # 垂直操作\n    FORWARD = \"up\"\n    BACKWARD = \"down\"\n\n    # 水平操作\n    HORIZ_FORWARD = \"left\"\n    HORIZ_BACKWARD = \"right\""
  },
  {
    "path": "uiautomator2/_selector.py",
    "content": "import logging\nimport time\nimport warnings\nfrom typing import Optional, Tuple, List, Dict\n\nfrom PIL import Image\nfrom retry import retry\n\nfrom uiautomator2._proto import SCROLL_STEPS\nfrom uiautomator2.exceptions import HTTPError, UiObjectNotFoundError\nfrom uiautomator2.utils import Exists, intersect\n\n\nclass Selector(dict):\n    \"\"\"The class is to build parameters for UiSelector passed to Android device.\n    \"\"\"\n    __fields = {\n        \"text\": (0x01, None),  # MASK_TEXT,\n        \"textContains\": (0x02, None),  # MASK_TEXTCONTAINS,\n        \"textMatches\": (0x04, None),  # MASK_TEXTMATCHES,\n        \"textStartsWith\": (0x08, None),  # MASK_TEXTSTARTSWITH,\n        \"className\": (0x10, None),  # MASK_CLASSNAME\n        \"classNameMatches\": (0x20, None),  # MASK_CLASSNAMEMATCHES\n        \"description\": (0x40, None),  # MASK_DESCRIPTION\n        \"descriptionContains\": (0x80, None),  # MASK_DESCRIPTIONCONTAINS\n        \"descriptionMatches\": (0x0100, None),  # MASK_DESCRIPTIONMATCHES\n        \"descriptionStartsWith\": (0x0200, None),  # MASK_DESCRIPTIONSTARTSWITH\n        \"checkable\": (0x0400, False),  # MASK_CHECKABLE\n        \"checked\": (0x0800, False),  # MASK_CHECKED\n        \"clickable\": (0x1000, False),  # MASK_CLICKABLE\n        \"longClickable\": (0x2000, False),  # MASK_LONGCLICKABLE,\n        \"scrollable\": (0x4000, False),  # MASK_SCROLLABLE,\n        \"enabled\": (0x8000, False),  # MASK_ENABLED,\n        \"focusable\": (0x010000, False),  # MASK_FOCUSABLE,\n        \"focused\": (0x020000, False),  # MASK_FOCUSED,\n        \"selected\": (0x040000, False),  # MASK_SELECTED,\n        \"packageName\": (0x080000, None),  # MASK_PACKAGENAME,\n        \"packageNameMatches\": (0x100000, None),  # MASK_PACKAGENAMEMATCHES,\n        \"resourceId\": (0x200000, None),  # MASK_RESOURCEID,\n        \"resourceIdMatches\": (0x400000, None),  # MASK_RESOURCEIDMATCHES,\n        \"index\": (0x800000, 0),  # MASK_INDEX,\n        \"instance\": (0x01000000, 0)  # MASK_INSTANCE,\n    }\n    __mask, __childOrSibling, __childOrSiblingSelector = \"mask\", \"childOrSibling\", \"childOrSiblingSelector\"\n\n    def __init__(self, **kwargs):\n        super(Selector, self).__setitem__(self.__mask, 0)\n        super(Selector, self).__setitem__(self.__childOrSibling, [])\n        super(Selector, self).__setitem__(self.__childOrSiblingSelector, [])\n        for k in kwargs:\n            self[k] = kwargs[k]\n\n    def __str__(self):\n        \"\"\" remove useless part for easily debugger \"\"\"\n        selector = self.copy()\n        selector.pop('mask')\n        for key in ('childOrSibling', 'childOrSiblingSelector'):\n            if not selector.get(key):\n                selector.pop(key)\n        args = []\n        for (k, v) in selector.items():\n            args.append(k + '=' + repr(v))\n        return 'Selector [' + ', '.join(args) + ']'\n\n    def __setitem__(self, k, v):\n        if k in self.__fields:\n            super(Selector, self).__setitem__(k, v)\n            super(Selector,\n                  self).__setitem__(self.__mask,\n                                    self[self.__mask] | self.__fields[k][0])\n        else:\n            raise ReferenceError(\"%s is not allowed.\" % k)\n\n    def __delitem__(self, k):\n        if k in self.__fields:\n            super(Selector, self).__delitem__(k)\n            super(Selector,\n                  self).__setitem__(self.__mask,\n                                    self[self.__mask] & ~self.__fields[k][0])\n\n    def clone(self):\n        kwargs = dict((k, self[k]) for k in self if k not in [\n            self.__mask, self.__childOrSibling, self.__childOrSiblingSelector\n        ])\n        selector = Selector(**kwargs)\n        for v in self[self.__childOrSibling]:\n            selector[self.__childOrSibling].append(v)\n        for s in self[self.__childOrSiblingSelector]:\n            selector[self.__childOrSiblingSelector].append(s.clone())\n        return selector\n\n    def child(self, **kwargs):\n        self[self.__childOrSibling].append(\"child\")\n        self[self.__childOrSiblingSelector].append(Selector(**kwargs))\n        return self\n\n    def sibling(self, **kwargs):\n        self[self.__childOrSibling].append(\"sibling\")\n        self[self.__childOrSiblingSelector].append(Selector(**kwargs))\n        return self\n\n    def update_instance(self, i):\n        # update inside child instance\n        if self[self.__childOrSiblingSelector]:\n            self[self.__childOrSiblingSelector][-1]['instance'] = i\n        else:\n            self['instance'] = i\n\n\nclass UiObject(object):\n    def __init__(self, session, selector: Selector):\n        self.session = session\n        self.selector = selector\n        self.jsonrpc = session.jsonrpc\n\n    @property\n    def wait_timeout(self):\n        return self.session.wait_timeout\n\n    @property\n    def exists(self):\n        '''check if the object exists in current window.'''\n        return Exists(self)\n\n    @property\n    def info(self):\n        '''ui object info.'''\n        return self.jsonrpc.objInfo(self.selector)\n\n    def info_list(self) -> List[Dict]:\n        '''all matched ui objects info list.'''\n        return self.jsonrpc.objInfoOfAllInstances(self.selector)\n\n    def screenshot(self, display_id: Optional[int] = None) -> Image.Image:\n        im = self.session.screenshot(display_id=display_id)\n        return im.crop(self.bounds())\n\n    def click(self, timeout=None, offset=None):\n        \"\"\"\n        Click UI element. \n\n        Args:\n            timeout: seconds wait element show up\n            offset: (xoff, yoff) default (0.5, 0.5) -> center\n\n        The click method does the same logic as java uiautomator does.\n        1. waitForExists 2. get VisibleBounds center 3. send click event\n\n        Raises:\n            UiObjectNotFoundError\n        \"\"\"\n        # self.jsonrpc.click(self.selector)\n        self.must_wait(timeout=timeout)\n        x, y = self.center(offset=offset)\n        self.session.click(x, y)\n\n    def bounds(self) -> Tuple[int, int, int, int]:\n        \"\"\"\n        Returns:\n            left_top_x, left_top_y, right_bottom_x, right_bottom_y\n        \"\"\"\n        info = self.info\n        bounds = info.get('visibleBounds') or info.get(\"bounds\")\n        lx, ly, rx, ry = bounds['left'], bounds['top'], bounds['right'], bounds['bottom'] # yapf: disable\n        return (lx, ly, rx, ry)\n\n    def center(self, offset=(0.5, 0.5)):\n        \"\"\"\n        Args:\n            offset: optional, (x_off, y_off)\n                (0, 0) means left-top, (0.5, 0.5) means middle(Default)\n        Return:\n            center point (x, y)\n        \"\"\"\n        lx, ly, rx, ry = self.bounds()\n        if offset is None:\n            offset = (0.5, 0.5)  # default center\n        xoff, yoff = offset\n        width, height = rx - lx, ry - ly\n        x = lx + width * xoff\n        y = ly + height * yoff\n        return (x, y)\n\n    def click_gone(self, maxretry=10, interval=1.0):\n        \"\"\"\n        Click until element is gone\n\n        Args:\n            maxretry (int): max click times\n            interval (float): sleep time between clicks\n\n        Return:\n            Bool if element is gone\n        \"\"\"\n        self.click_exists()\n        while maxretry > 0:\n            time.sleep(interval)\n            if not self.exists:\n                return True\n            self.click_exists()\n            maxretry -= 1\n        return False\n\n    def click_exists(self, timeout=0) -> bool:\n        try:\n            self.click(timeout=timeout)\n            return True\n        except UiObjectNotFoundError:\n            return False\n\n    def long_click(self, duration: float = 0.5, timeout=None):\n        \"\"\"\n        Args:\n            duration (float): seconds of pressed\n            timeout (float): seconds wait element show up\n        \"\"\"\n\n        # if info['longClickable'] and not duration:\n        #     return self.jsonrpc.longClick(self.selector)\n        self.must_wait(timeout=timeout)\n        x, y = self.center()\n        return self.session.long_click(x, y, duration)\n\n    def drag_to(self, *args, **kwargs):\n        duration = kwargs.pop('duration', 0.5)\n        timeout = kwargs.pop('timeout', None)\n        self.must_wait(timeout=timeout)\n\n        steps = int(duration * 200)\n        if len(args) >= 2 or \"x\" in kwargs or \"y\" in kwargs:\n\n            def drag2xy(x, y):\n                x, y = self.session.pos_rel2abs(x,\n                                                y)  # convert percent position\n                return self.jsonrpc.dragTo(self.selector, x, y, steps)\n\n            return drag2xy(*args, **kwargs)\n        return self.jsonrpc.dragTo(self.selector, Selector(**kwargs), steps)\n\n    def swipe(self, direction, steps=10):\n        \"\"\"\n        Performs the swipe action on the UiObject.\n        Swipe from center\n\n        Args:\n            direction (str): one of (\"left\", \"right\", \"up\", \"down\")\n            steps (int): move steps, one step is about 5ms\n            percent: float between [0, 1]\n\n        Note: percent require API >= 18\n        # assert 0 <= percent <= 1\n        \"\"\"\n        assert direction in (\"left\", \"right\", \"up\", \"down\")\n\n        self.must_wait()\n        info = self.info\n        bounds = info.get('visibleBounds') or info.get(\"bounds\")\n        lx, ly, rx, ry = bounds['left'], bounds['top'], bounds['right'], bounds['bottom'] # yapf: disable\n        cx, cy = (lx + rx) // 2, (ly + ry) // 2\n        if direction == 'up':\n            self.session.swipe(cx, cy, cx, ly, steps=steps)\n        elif direction == 'down':\n            self.session.swipe(cx, cy, cx, ry - 1, steps=steps)\n        elif direction == 'left':\n            self.session.swipe(cx, cy, lx, cy, steps=steps)\n        elif direction == 'right':\n            self.session.swipe(cx, cy, rx - 1, cy, steps=steps)\n\n        # return self.jsonrpc.swipe(self.selector, direction, percent, steps)\n\n    def gesture(self, start1, start2, end1, end2, steps=100):\n        '''\n        perform two point gesture.\n        Usage:\n        d().gesture(startPoint1, startPoint2, endPoint1, endPoint2, steps)\n        '''\n        rel2abs = self.session.pos_rel2abs\n\n        def point(x=0, y=0):\n            x, y = rel2abs(x, y)\n            return {\"x\": x, \"y\": y}\n\n        def ctp(pt):\n            return point(*pt) if type(pt) == tuple else pt\n\n        s1, s2, e1, e2 = ctp(start1), ctp(start2), ctp(end1), ctp(end2)\n        return self.jsonrpc.gesture(self.selector, s1, s2, e1, e2, steps)\n\n    def pinch_in(self, percent=100, steps=50):\n        return self.jsonrpc.pinchIn(self.selector, percent, steps)\n\n    def pinch_out(self, percent=100, steps=50):\n        return self.jsonrpc.pinchOut(self.selector, percent, steps)\n\n    def wait(self, exists=True, timeout=None):\n        \"\"\"\n        Wait until UI Element exists or gone\n\n        Args:\n            timeout (float): wait element timeout\n\n        Example:\n            d(text=\"Clock\").wait()\n            d(text=\"Settings\").wait(exists=False) # wait until it's gone\n        \"\"\"\n        if timeout is None:\n            timeout = self.wait_timeout\n        http_wait = timeout + 10\n        if exists:\n            try:\n                return self.jsonrpc.waitForExists(self.selector,\n                                                  int(timeout * 1000),\n                                                  http_timeout=http_wait)\n            except HTTPError as e:\n                warnings.warn(\"waitForExists readTimeout: %s\" % e,\n                              RuntimeWarning)\n                return self.exists()\n        else:\n            try:\n                return self.jsonrpc.waitUntilGone(self.selector,\n                                                  int(timeout * 1000),\n                                                  http_timeout=http_wait)\n            except HTTPError as e:\n                warnings.warn(\"waitForExists readTimeout: %s\" % e,\n                              RuntimeWarning)\n                return not self.exists()\n\n    def wait_gone(self, timeout=None):\n        \"\"\" wait until ui gone\n        Args:\n            timeout (float): wait element gone timeout\n\n        Returns:\n            bool if element gone\n        \"\"\"\n        timeout = timeout or self.wait_timeout\n        return self.wait(exists=False, timeout=timeout)\n\n    def must_wait(self, exists=True, timeout=None):\n        \"\"\" wait and if not found raise UiObjectNotFoundError \"\"\"\n        if not self.wait(exists, timeout):\n            raise UiObjectNotFoundError({'code': -32002, 'data': str(self.selector), 'method': 'wait'})\n\n    def send_keys(self, text):\n        \"\"\" alias of set_text \"\"\"\n        return self.set_text(text)\n\n    def set_text(self, text, timeout=None):\n        self.must_wait(timeout=timeout)\n        if not text:\n            return self.jsonrpc.clearTextField(self.selector)\n        else:\n            return self.jsonrpc.setText(self.selector, text)\n\n    def get_text(self, timeout=None):\n        \"\"\" get text from field \"\"\"\n        self.must_wait(timeout=timeout)\n        return self.jsonrpc.getText(self.selector)\n\n    def clear_text(self, timeout=None):\n        self.must_wait(timeout=timeout)\n        return self.set_text(None)\n\n    def child(self, **kwargs):\n        return UiObject(self.session, self.selector.clone().child(**kwargs))\n\n    def sibling(self, **kwargs):\n        return UiObject(self.session, self.selector.clone().sibling(**kwargs))\n\n    child_selector, from_parent = child, sibling\n\n    def child_by_text(self, txt, **kwargs):\n        if \"allow_scroll_search\" in kwargs:\n            allow_scroll_search = kwargs.pop(\"allow_scroll_search\")\n            name = self.jsonrpc.childByText(self.selector, Selector(**kwargs),\n                                            txt, allow_scroll_search)\n        else:\n            name = self.jsonrpc.childByText(self.selector, Selector(**kwargs),\n                                            txt)\n        return UiObject(self.session, name)\n\n    def child_by_description(self, txt, **kwargs):\n        # need test\n        if \"allow_scroll_search\" in kwargs:\n            allow_scroll_search = kwargs.pop(\"allow_scroll_search\")\n            name = self.jsonrpc.childByDescription(self.selector,\n                                                   Selector(**kwargs), txt,\n                                                   allow_scroll_search)\n        else:\n            name = self.jsonrpc.childByDescription(self.selector,\n                                                   Selector(**kwargs), txt)\n        return UiObject(self.session, name)\n\n    def child_by_instance(self, inst, **kwargs):\n        # need test\n        return UiObject(\n            self.session,\n            self.jsonrpc.childByInstance(self.selector, Selector(**kwargs),\n                                         inst))\n\n    def parent(self):\n        # android-uiautomator-server not implemented\n        # In UIAutomator, UIObject2 has getParent() method\n        # https://developer.android.com/reference/android/support/test/uiautomator/UiObject2.html\n        raise NotImplementedError()\n        # return UiObject(self.session, self.jsonrpc.getParent(self.selector))\n\n    def __getitem__(self, instance: int):\n        \"\"\"\n        Raises:\n            IndexError\n        \"\"\"\n        if isinstance(self.selector, str):\n            raise IndexError(\n                \"Index is not supported when UiObject returned by child_by_xxx\"\n            )\n        selector = self.selector.clone()\n        if instance < 0:\n            selector['instance'] = 0\n            del selector['instance']\n            count = self.jsonrpc.count(selector)\n            assert instance + count >= 0\n            instance += count\n\n        selector.update_instance(instance)\n        return UiObject(self.session, selector)\n\n    @property\n    def count(self):\n        return self.jsonrpc.count(self.selector)\n\n    def __len__(self):\n        return self.count\n\n    def __iter__(self):\n        obj, length = self, self.count\n\n        class Iter(object):\n            def __init__(self):\n                self.index = -1\n\n            def next(self):\n                self.index += 1\n                if self.index < length:\n                    return obj[self.index]\n                else:\n                    raise StopIteration()\n\n            __next__ = next\n\n        return Iter()\n\n    def right(self, **kwargs):\n        def onrightof(rect1, rect2):\n            left, top, right, bottom = intersect(rect1, rect2)\n            return rect2[\"left\"] - rect1[\"right\"] if top < bottom else -1\n\n        return self.__view_beside(onrightof, **kwargs)\n\n    def left(self, **kwargs):\n        def onleftof(rect1, rect2):\n            left, top, right, bottom = intersect(rect1, rect2)\n            return rect1[\"left\"] - rect2[\"right\"] if top < bottom else -1\n\n        return self.__view_beside(onleftof, **kwargs)\n\n    def up(self, **kwargs):\n        def above(rect1, rect2):\n            left, top, right, bottom = intersect(rect1, rect2)\n            return rect1[\"top\"] - rect2[\"bottom\"] if left < right else -1\n\n        return self.__view_beside(above, **kwargs)\n\n    def down(self, **kwargs):\n        def under(rect1, rect2):\n            left, top, right, bottom = intersect(rect1, rect2)\n            return rect2[\"top\"] - rect1[\"bottom\"] if left < right else -1\n\n        return self.__view_beside(under, **kwargs)\n\n    def __view_beside(self, onsideof, **kwargs):\n        bounds = self.info[\"bounds\"]\n        min_dist, found = -1, None\n        info_list = UiObject(self.session, Selector(**kwargs)).info_list()\n        for index, info in enumerate(info_list):\n            dist = onsideof(bounds, info[\"bounds\"])\n            if dist >= 0 and (min_dist < 0 or dist < min_dist):\n                ui_selector = Selector(**kwargs)\n                ui_selector.update_instance(index)\n                min_dist, found = dist, UiObject(self.session, ui_selector)\n        return found\n\n    @property\n    def fling(self):\n        \"\"\"\n        Args:\n            dimention (str): one of \"vert\", \"vertically\", \"vertical\", \"horiz\", \"horizental\", \"horizentally\"\n            action (str): one of \"forward\", \"backward\", \"toBeginning\", \"toEnd\", \"to\"\n        \"\"\"\n        jsonrpc = self.jsonrpc\n        selector = self.selector\n\n        class _Fling(object):\n            def __init__(self):\n                self._vertical = True\n                self.action = 'forward'\n\n            def __getattr__(self, key):\n                if key in [\"horiz\", \"horizental\", \"horizentally\"]:\n                    self._vertical = False\n                    return self\n                if key in ['vert', 'vertically', 'vertical']:\n                    self._vertical = True\n                    return self\n                if key in [\n                        \"forward\", \"backward\", \"toBeginning\", \"toEnd\", \"to\"\n                ]:\n                    self.action = key\n                    return self\n                raise ValueError(\"invalid prop %s\" % key)\n\n            def __call__(self, max_swipes=500, **kwargs):\n                if self.action == \"forward\":\n                    return jsonrpc.flingForward(selector, self._vertical)\n                elif self.action == \"backward\":\n                    return jsonrpc.flingBackward(selector, self._vertical)\n                elif self.action == \"toBeginning\":\n                    return jsonrpc.flingToBeginning(selector, self._vertical,\n                                                    max_swipes)\n                elif self.action == \"toEnd\":\n                    return jsonrpc.flingToEnd(selector, self._vertical,\n                                              max_swipes)\n\n        return _Fling()\n\n    @property\n    def scroll(self):\n        \"\"\"\n        Args:\n            dimention (str): one of \"vert\", \"vertically\", \"vertical\", \"horiz\", \"horizental\", \"horizentally\"\n            action (str): one of \"forward\", \"backward\", \"toBeginning\", \"toEnd\", \"to\"\n        \"\"\"\n        selector = self.selector\n        jsonrpc = self.jsonrpc\n\n        class _Scroll(object):\n            def __init__(self):\n                self._vertical = True\n                self.action = 'forward'\n\n            def __getattr__(self, key):\n                if key in [\"horiz\", \"horizental\", \"horizentally\"]:\n                    self._vertical = False\n                    return self\n                if key in ['vert', 'vertically', 'vertical']:\n                    self._vertical = True\n                    return self\n                if key in [\n                        \"forward\", \"backward\", \"toBeginning\", \"toEnd\", \"to\"\n                ]:\n                    self.action = key\n                    return self\n                raise ValueError(\"invalid prop %s\" % key)\n\n            def __call__(self, steps=SCROLL_STEPS, max_swipes=500, **kwargs):\n                # More steps slows the swipe and prevents contents from being flung too far\n                if self.action in [\"forward\", \"backward\"]:\n                    method = jsonrpc.scrollForward if self.action == \"forward\" else jsonrpc.scrollBackward\n                    return method(selector, self._vertical, steps)\n                elif self.action == \"toBeginning\":\n                    return jsonrpc.scrollToBeginning(selector, self._vertical,\n                                                     max_swipes, steps)\n                elif self.action == \"toEnd\":\n                    return jsonrpc.scrollToEnd(selector, self._vertical,\n                                               max_swipes, steps)\n                elif self.action == \"to\":\n                    return jsonrpc.scrollTo(selector, Selector(**kwargs),\n                                            self._vertical)\n\n        return _Scroll()\n"
  },
  {
    "path": "uiautomator2/abstract.py",
    "content": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\n\n\"\"\"Created on Thu Apr 25 2024 15:08:43 by codeskyblue\n\"\"\"\n\nimport abc\nimport typing\nfrom typing import Any, List, NamedTuple, Tuple, Union\n\nimport adbutils\nfrom PIL import Image\n\nfrom uiautomator2._proto import Direction\n\n\nclass ShellResponse(NamedTuple):\n    output: str\n    exit_code: int\n    \n    \n\nclass AbstractUiautomatorServer(abc.ABC):\n    @abc.abstractmethod\n    def start_uiautomator(self):\n        pass\n\n    @abc.abstractmethod\n    def stop_uiautomator(self):\n        pass\n\n    @abc.abstractmethod\n    def jsonrpc_call(self, method: str, params: Any = None) -> Any:\n        pass\n\n\n\nclass AbstractShell(abc.ABC):\n    @abc.abstractmethod\n    def shell(self, cmdargs: Union[List[str], str]) -> ShellResponse:\n        pass\n\n    @property\n    @abc.abstractmethod\n    def adb_device(self) -> adbutils.AdbDevice:\n        pass\n    \n    @property\n    @abc.abstractmethod\n    def jsonrpc(self) -> typing.Any:\n        pass\n    \n\nclass AbstractXPathBasedDevice(metaclass=abc.ABCMeta):\n    @abc.abstractmethod\n    def click(self, x: int, y: int):\n        pass\n    \n    @abc.abstractmethod\n    def long_click(self, x: int, y: int):\n        pass\n\n    @abc.abstractmethod\n    def send_keys(self, text: str):\n        pass\n\n    @abc.abstractmethod\n    def swipe(self, fx: int, fy: int, tx: int, ty: int, duration: float):\n        \"\"\" duration is float type, indicate seconds \"\"\"\n    \n    @abc.abstractmethod\n    def swipe_ext(self, direction: Direction, scale: float):\n        pass\n    \n    @abc.abstractmethod\n    def window_size(self) -> Tuple[int, int]:\n        \"\"\" return (width, height) \"\"\"\n    \n    @abc.abstractmethod\n    def dump_hierarchy(self) -> str:\n        \"\"\" return xml content \"\"\"\n    \n    @abc.abstractmethod\n    def screenshot(self) -> Image.Image:\n        \"\"\" return PIL.Image.Image \"\"\"\n"
  },
  {
    "path": "uiautomator2/assets/.gitignore",
    "content": "*.apk\natx-agent\nversion.json\n*.jar"
  },
  {
    "path": "uiautomator2/assets/sync.sh",
    "content": "#!/bin/bash\n#\n\nset -e\n\nAPK_VERSION=$(cat ../version.py| grep apk_version | awk '{print $NF}')\nAPK_VERSION=${APK_VERSION//[\\\"\\']}\nJAR_VERSION=\"0.2.2\"\n\ncd \"$(dirname $0)\"\n\n\nfunction download() {\n\tlocal URL=$1\n\tlocal OUTPUT=$2\n\techo \">> download $URL -> $OUTPUT\"\n\tcurl -L \"$URL\" --output \"$OUTPUT\"\n}\n\nfunction download_apk(){\n\tlocal VERSION=$1\n\tlocal NAME=$2\n\tlocal URL=\"https://github.com/openatx/android-uiautomator-server/releases/download/$VERSION/$NAME\"\n\tdownload \"$URL\" \"$NAME\"\n\tunzip -tq \"$NAME\"\n}\n\nfunction download_jar() {\n\tlocal URL=\"https://public.uiauto.devsleep.com/u2jar/$JAR_VERSION/u2.jar\"\n\thttps_proxy= download \"$URL\" \"u2.jar\"\n\tif test -s u2.jar; then\n\t\techo \"Download Jar sucessfully\"\n\telse\n\t\techo \"Download Jar failed\"\n\t\texit 1\n\tfi\n}\n\necho \"APK_VERSION: $APK_VERSION\"\n\ndownload_jar\ndownload_apk \"$APK_VERSION\" \"app-uiautomator.apk\"\ncat > version.json <<EOF\n{\n  \"com.github.uiautomator\": \"$APK_VERSION\"\n}\nEOF\n"
  },
  {
    "path": "uiautomator2/base.py",
    "content": "from __future__ import absolute_import, print_function\n\nimport logging\nimport re\nimport time\nfrom functools import cached_property\nfrom typing import Any, Dict, List, Optional, Tuple, Union\n\nimport adbutils\n\nfrom uiautomator2._proto import HTTP_TIMEOUT, SCROLL_STEPS, Direction\nfrom uiautomator2.abstract import ShellResponse\nfrom uiautomator2.core import BasicUiautomatorServer\nfrom uiautomator2.exceptions import *\nfrom uiautomator2.settings import Settings\nfrom uiautomator2.utils import deprecated, image_convert, list2cmdline\n\nlogger = logging.getLogger(__name__)\n\n\nclass _BaseClient(BasicUiautomatorServer):\n    \"\"\"\n    提供最基础的控制类，这个类暂时先不公开吧\n    \"\"\"\n\n    def __init__(self, serial: Optional[Union[str, adbutils.AdbDevice]] = None):\n        \"\"\"\n        Args:\n            serial: device serialno\n        \"\"\"\n        if isinstance(serial, adbutils.AdbDevice):\n            self.__serial = serial.serial\n            self._dev = serial\n        else:\n            self.__serial = serial\n            self._dev = self._wait_for_device()\n        self._debug = False\n        BasicUiautomatorServer.__init__(self, self._dev)\n    \n    @property\n    def _serial(self) -> str:\n        return self.__serial\n    \n    def _wait_for_device(self, timeout=10) -> adbutils.AdbDevice:\n        \"\"\"\n        wait for device came online, if device is remote, reconnect every 1s\n\n        Returns:\n            adbutils.AdbDevice\n        \n        Raises:\n            ConnectError\n        \"\"\"\n        for d in adbutils.adb.device_list():\n            if d.serial == self._serial:\n                return d\n\n        _RE_remote_adb = re.compile(r\"^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}:\\d+$\")\n        _is_remote = _RE_remote_adb.match(self._serial) is not None\n\n        adb = adbutils.adb\n        deadline = time.time() + timeout\n        while time.time() < deadline:\n            title = \"device reconnecting\" if _is_remote else \"wait-for-device\"\n            logger.debug(\"%s, time left(%.1fs)\", title, deadline - time.time())\n            if _is_remote:\n                try:\n                    adb.disconnect(self._serial)\n                    adb.connect(self._serial, timeout=1)\n                except (adbutils.AdbError, adbutils.AdbTimeout) as e:\n                    logger.debug(\"adb reconnect error: %s\", str(e))\n                    time.sleep(1.0)\n                    continue\n            try:\n                adb.wait_for(self._serial, timeout=1)\n            except (adbutils.AdbError, adbutils.AdbTimeout):\n                continue\n            return adb.device(self._serial)\n        raise ConnectError(f\"device {self._serial} not online\")\n\n    @property\n    def adb_device(self) -> adbutils.AdbDevice:\n        return self._dev\n    \n    @cached_property\n    def settings(self) -> Settings:\n        return Settings(self)\n\n    def sleep(self, seconds: float):\n        \"\"\" same as time.sleep \"\"\"\n        time.sleep(seconds)\n\n    def shell(self, cmdargs: Union[str, List[str]], timeout=60) -> ShellResponse:\n        \"\"\"\n        Run shell command on device\n\n        Args:\n            cmdargs: str or list, example: \"ls -l\" or [\"ls\", \"-l\"]\n            timeout: seconds of command run, works on when stream is False\n\n        Returns:\n            ShellResponse\n\n        Raises:\n            AdbShellError\n        \"\"\"\n        try:\n            if self.debug:\n                print(\"shell:\", list2cmdline(cmdargs))\n            logger.debug(\"shell: %s\", list2cmdline(cmdargs))\n            ret = self._dev.shell2(cmdargs, timeout=timeout)\n            return ShellResponse(ret.output, ret.returncode)\n        except adbutils.AdbError as e:\n            raise AdbShellError(e)\n\n    @property\n    def info(self) -> Dict[str, Any]:\n        return self.jsonrpc.deviceInfo(http_timeout=10)\n    \n    @property\n    def device_info(self) -> Dict[str, Any]:\n        serial = self._dev.getprop(\"ro.serialno\")\n        sdk = self._dev.getprop(\"ro.build.version.sdk\")\n        version = self._dev.getprop(\"ro.build.version.release\")\n        brand = self._dev.getprop(\"ro.product.brand\")\n        model = self._dev.getprop(\"ro.product.model\")\n        arch = self._dev.getprop(\"ro.product.cpu.abi\")\n        return {\n            \"serial\": serial,\n            \"sdk\": int(sdk) if sdk.isdigit() else None,\n            \"brand\": brand,\n            \"model\": model,\n            \"arch\": arch,\n            \"version\": int(version) if version.isdigit() else None,\n        }\n\n    @property\n    def wlan_ip(self) -> Optional[str]:\n        try:\n            return self._dev.wlan_ip()\n        except adbutils.AdbError:\n            return None\n\n    @property\n    def jsonrpc(self):\n        class JSONRpcWrapper():\n            def __init__(self, server: BasicUiautomatorServer):\n                self.server = server\n                self.method = None\n\n            def __getattr__(self, method):\n                self.method = method  # jsonrpc function name\n                return self\n\n            def __call__(self, *args, **kwargs):\n                http_timeout = kwargs.pop('http_timeout', HTTP_TIMEOUT)\n                params = args if args else kwargs\n                return self.server.jsonrpc_call(self.method, params, http_timeout)\n\n        return JSONRpcWrapper(self)\n\n    def reset_uiautomator(self):\n        \"\"\"\n        restart uiautomator service\n\n        Orders:\n            - stop uiautomator keeper\n            - am force-stop com.github.uiautomator\n            - start uiautomator keeper(am instrument -w ...)\n            - wait until uiautomator service is ready\n        \"\"\"\n        self.stop_uiautomator()\n        self.start_uiautomator()\n\n    def push(self, src, dst: str, mode=0o644):\n        \"\"\"\n        Push file into device\n\n        Args:\n            src (path or fileobj): source file\n            dst (str): destination can be folder or file path\n            mode (int): file mode\n        \"\"\"\n        self._dev.sync.push(src, dst, mode=mode)\n\n    def pull(self, src: str, dst: str):\n        \"\"\"\n        Pull file from device to local\n        \"\"\"\n        try:\n            self._dev.sync.pull(src, dst, exist_ok=True)\n        except TypeError:\n            self._dev.sync.pull(src, dst)\n"
  },
  {
    "path": "uiautomator2/core.py",
    "content": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\n\n\"\"\"Created on Thu Apr 25 2024 14:50:05 by codeskyblue\n\"\"\"\n\nimport atexit\nimport datetime\nimport hashlib\nimport json\nimport logging\nimport os\nimport threading\nimport time\nfrom http.client import HTTPConnection\nfrom pathlib import Path\nfrom typing import Any, Dict, Optional, Union\n\nimport adbutils\nimport requests\n\nfrom uiautomator2.abstract import AbstractUiautomatorServer\nfrom uiautomator2.exceptions import AccessibilityServiceAlreadyRegisteredError, APKSignatureError, HTTPError, \\\n    HTTPTimeoutError, LaunchUiAutomationError, RPCInvalidError, RPCStackOverflowError, RPCUnknownError, \\\n    UiAutomationNotConnectedError, UiObjectNotFoundError\nfrom uiautomator2.utils import with_package_resource\nfrom uiautomator2.version import __apk_version__\n\nlogger = logging.getLogger(__name__)\n\nclass MockAdbProcess:\n    def __init__(self, conn: adbutils.AdbConnection) -> None:\n        self._conn = conn\n        self._event = threading.Event()\n        self._output = bytearray()\n        def wait_finished():\n            try:\n                while chunk := self._conn.conn.recv(1024):\n                    logger.debug(\"MockAdbProcess: %s\", chunk)\n                    self._output.extend(chunk)\n            except:\n                pass\n            self._event.set()\n        \n        t = threading.Thread(target=wait_finished)\n        t.daemon = True\n        t.name = \"wait_adb_conn\"\n        t.start()\n    \n    @property\n    def output(self) -> bytes:\n        \"\"\" subprocess do not have this property \"\"\"\n        return self._output\n\n    def wait(self) -> bool:\n        return self._event.wait(timeout=3)\n\n    def pool(self) -> Optional[int]:\n        if self._event.is_set():\n            return 0\n        return None\n\n    def kill(self):\n        self._conn.close()\n        self.wait()\n\n\ndef launch_uiautomator(dev: adbutils.AdbDevice) -> MockAdbProcess:\n    \"\"\"Launch uiautomator2 server on device\"\"\"\n    command = \"CLASSPATH=/data/local/tmp/u2.jar app_process / com.wetest.uia2.Main\"\n    logger.debug(\"launch uiautomator with cmd: %s\", command)\n    conn = dev.shell(command, stream=True)\n    process = MockAdbProcess(conn)\n    return process\n\n\nclass HTTPResponse:\n    def __init__(self, content: bytes) -> None:\n        self.content = content\n    \n    def json(self):\n        return json.loads(self.content)\n\n    @property\n    def text(self):\n        return self.content.decode(\"utf-8\", errors=\"ignore\")\n\n\nclass AdbHTTPConnection(HTTPConnection):\n    def __init__(self, device: adbutils.AdbDevice, port=9008):\n        super().__init__(\"localhost\", port)\n        self.__device = device\n        self.__port = port\n\n    def connect(self):\n        try:\n            self.sock = self.__device.create_connection(adbutils.Network.TCP, self.__port)\n        except adbutils.AdbError as e:\n            raise HTTPError(f\"Unable to connect to uiautomator2 server: {e}\") from e\n\n    def __enter__(self) -> HTTPConnection:\n        return self\n\n    def __exit__(self, exc_type, exc_value, traceback):\n        self.close()\n        \n\ndef _http_request(dev: adbutils.AdbDevice, device_port: int, method: str, path: str, data: Optional[Dict[str, Any]] = None, timeout=10.0, print_request: bool = False) -> HTTPResponse:\n    \"\"\"Send http request to uiautomator2 server\"\"\"\n    try:\n        logger.debug(\"http request %s %s %s\", method, path, data)\n\n        # https://stackoverflow.com/questions/2386299/running-sites-on-localhost-is-extremely-slow\n        # so here use 127.0.0.1 instead of localhost\n        if print_request:\n            start_time = datetime.datetime.now()\n            current_time = start_time.strftime(\"%H:%M:%S.%f\")[:-3]\n            url = f\"http://127.0.0.1:{device_port}{path}\"\n            fields = [current_time, f\"$ curl -X {method}\", url]\n            if data:\n                fields.append(f\"-d '{json.dumps(data)}'\")\n            print(f\"# http timeout={timeout}\")\n            print(\" \".join(fields))\n        \n        # set Accept-Encoding to empty to avoid gzip compression\n        # nanohttpd gzip has resource leaks\n        # https://github.com/NanoHttpd/nanohttpd/issues/492\n        # https://blog.csdn.net/fcp12138/article/details/80436644\n        headers = {\n            'User-Agent': 'uiautomator2',\n            'Accept-Encoding': '',\n            'Content-Type': 'application/json'\n        }\n        with AdbHTTPConnection(dev, port=device_port) as conn:\n            conn.timeout = timeout\n            if not data:\n                conn.request(method, path, headers=headers)\n            else:\n                conn.request(method, path, json.dumps(data), headers=headers)\n            _response = conn.getresponse()\n            content = bytearray()\n            while chunk := _response.read(4096):\n                content.extend(chunk)\n            if _response.status != 200:\n                raise HTTPError(f\"HTTP request failed: {_response.status} {_response.reason}\")\n            response = HTTPResponse(content)\n\n        if print_request:\n            end_time = datetime.datetime.now()\n            current_time = end_time.strftime(\"%H:%M:%S.%f\")[:-3]\n            print(f\"{current_time} Response >>>\")\n            print(response.text.rstrip())\n            print(f\"<<< END timed_used = %.3f\\n\" % (end_time - start_time).total_seconds())\n        return response\n    except requests.Timeout as e:\n        raise HTTPTimeoutError(f\"HTTP request timeout: {e}\") from e\n    except requests.RequestException as e:\n        raise HTTPError(f\"HTTP request failed: {e}\") from e\n\n\ndef _jsonrpc_call(dev: adbutils.AdbDevice, device_port: int, method: str, params: Any, timeout: float, print_request: bool) -> Any:\n    \"\"\"Send jsonrpc call to uiautomator2 server\n    \n    Raises:\n        UiAutomationError\n    \"\"\"\n    payload = {\n        \"jsonrpc\": \"2.0\",\n        \"id\": 1,\n        \"method\": method,\n        \"params\": params\n    }\n    r = _http_request(dev, device_port, \"POST\", \"/jsonrpc/0\", payload, timeout=timeout, print_request=print_request)\n    data = r.json()\n    if not isinstance(data, dict):\n        raise RPCInvalidError(\"Unknown RPC error: not a dict\")\n    \n    if isinstance(data, dict) and \"error\" in data:\n        logger.debug(\"jsonrpc error: %s\", data)\n        code = data['error'].get('code')\n        message = data['error'].get('message', '')\n        stacktrace = data['error'].get('data')\n        if \"UiAutomation not connected\" in r.text:\n            raise UiAutomationNotConnectedError(\"UiAutomation not connected\")\n        if \"android.os.DeadObjectException\" in message:\n            # https://developer.android.com/reference/android/os/DeadObjectException\n            raise UiAutomationNotConnectedError(\"android.os.DeadObjectException\")\n        if \"android.os.DeadSystemRuntimeException\" in message:\n            raise UiAutomationNotConnectedError(\"android.os.DeadSystemRuntimeException\")\n        if \"uiautomator.UiObjectNotFoundException\" in message:\n            raise UiObjectNotFoundError(code, message, params)\n        if \"java.lang.StackOverflowError\" in message:\n            raise RPCStackOverflowError(f\"StackOverflowError: {message}\", params, stacktrace[:1000] + \"...\" + stacktrace[-1000:])\n        raise RPCUnknownError(f\"Unknown RPC error: {code} {message}\", params, stacktrace)\n    \n    if \"result\" not in data:\n        raise RPCInvalidError(\"Unknown RPC error: no result field\")\n    return data[\"result\"]\n\n\nclass BasicUiautomatorServer(AbstractUiautomatorServer):\n    \"\"\" Simple uiautomator2 server client\n    this is runs without atx-agent\n    \"\"\"\n    _lock = threading.Lock() # thread safe lock\n    \n    def __init__(self, dev: adbutils.AdbDevice, device_server_port: int = 9008) -> None:\n        self._dev = dev\n        self._process = None\n        self._debug = False\n        self._device_server_port = device_server_port\n        self.start_uiautomator()\n        atexit.register(self.stop_uiautomator, wait=False)\n    \n    @property\n    def debug(self) -> bool:\n        return self._debug\n\n    @debug.setter\n    def debug(self, value: bool):\n        self._debug = bool(value)\n\n    def start_uiautomator(self):\n        \"\"\"\n        Start uiautomator2 server\n\n        Raises:\n            LaunchUiautomatorError: uiautomator2 server not ready\n        \"\"\"\n        with self._lock:\n            self._setup_jar()\n            if self._process:\n                if self._process.pool() is not None:\n                    self._process = None\n            if not self._check_alive():\n                self._process = launch_uiautomator(self._dev)\n                self._wait_ready()\n\n    def _setup_jar(self):\n        with with_package_resource(\"assets/u2.jar\") as jar_path:\n            target_path = \"/data/local/tmp/u2.jar\"\n            if self._check_device_file_hash(jar_path, target_path):\n                logger.debug(\"file u2.jar already pushed\")\n            else:\n                logger.debug(\"push %s -> %s\", jar_path, target_path)\n                self._dev.sync.push(jar_path, target_path, check=True)\n    \n    def _check_device_file_hash(self, local_file: Union[str, Path], remote_file: str) -> bool:\n        \"\"\" check if remote file hash is correct \"\"\"\n        md5 = hashlib.md5()\n        with open(local_file, \"rb\") as f:\n            md5.update(f.read())\n        local_md5 = md5.hexdigest()\n        logger.debug(\"file %s md5: %s\", os.path.basename(local_file), local_md5)\n        output = self._dev.shell([\"toybox\", \"md5sum\", remote_file])\n        if \"toybox\" in output and \"not found\" in output:\n            output = self._dev.shell([\"md5\", remote_file])\n        return local_md5 in output\n\n    def _wait_ready(self, launch_timeout=30):\n        \"\"\"Wait until uiautomator2 server is ready\"\"\"\n        self._wait_app_process_ready(launch_timeout)\n    \n    def _wait_app_process_ready(self, timeout: float):\n        \"\"\"\n        ERROR1:\n            [server] INFO: [UiAutomator2Server] Starting Server\n            java.lang.IllegalStateException: UiAutomationService android.accessibilityservice.IAccessibilityServiceClient$Stub$Proxy@5deffd5already registered!\n\n        NORMAL:\n            [server] INFO: [UiAutomator2Server] Starting Server\n            SLF4J: Failed to load class \"org.slf4j.impl.StaticLoggerBinder\".\n            SLF4J: Defaulting to no-operation (NOP) logger implementation\n            SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.\n        \"\"\"\n        deadline = time.time() + timeout\n        output_buffer = ''\n        while time.time() < deadline:\n            output = self._process.output.decode(\"utf-8\", errors=\"ignore\")\n            output_buffer += output\n            if \"already registered\" in output:\n                raise AccessibilityServiceAlreadyRegisteredError(output)\n            if self._process.pool() is not None:\n                raise LaunchUiAutomationError(\"server quit unexpectly\", output_buffer)\n            if self._check_alive():\n                return\n            time.sleep(.5)\n        raise LaunchUiAutomationError(\"server not ready\", output_buffer)\n\n    def _check_alive(self) -> bool:\n        try:\n            response = _http_request(self._dev, self._device_server_port, \"GET\", \"/ping\")\n            return response.content == b\"pong\"\n        except (HTTPError, ConnectionError):\n            return False\n    \n    def stop_uiautomator(self, wait=True):\n        with self._lock:\n            if self._process:\n                self._process.kill()\n                self._process = None\n        # wait server quit\n        if wait:\n            deadline = time.time() + 10\n            while time.time() < deadline:\n                if not self._check_alive():\n                    return\n                time.sleep(.5)\n\n    def jsonrpc_call(self, method: str, params: Any = None, timeout: float = 10) -> Any:\n        \"\"\"Send jsonrpc call to uiautomator2 server\"\"\"\n        try:\n            return _jsonrpc_call(self._dev, self._device_server_port, method, params, timeout, self._debug)\n        except (HTTPError, UiAutomationNotConnectedError) as e:\n            logger.debug(\"uiautomator2 is not ok, error: %s\", e)\n            self.stop_uiautomator()\n            self.start_uiautomator()\n            return _jsonrpc_call(self._dev, self._device_server_port, method, params, timeout, self._debug)\n"
  },
  {
    "path": "uiautomator2/exceptions.py",
    "content": "# coding: utf-8\n#\n# BaseException\n#   +- RPCError\n#   |   +- RPCUnknownError\n#   |   +- RPCInvalidError\n#   |   +- HierarchyEmptyError\n#   |   +- RPCStackOverflowError\n#   |   +- NormalError\n#   |     +- XPathElementNotFoundError\n#   |     +- UiObjectNotFoundError\n#   |     +- AppNotFoundError\n#   |     +- SessionBrokenError  \n#   +- DeviceError\n#      +- InputIMEError\n#      +- HTTPError\n#      +- ConnectError\n#      +- AdbShellError\n#      +- AdbBroadcastError\n#      +- APKSignatureError\n#      +- UiAutomationError\n#         +- UiAutomationNotConnectedError\n#         +- InjectPermissionError\n#         +- LaunchUiAutomationError\n#         +- AccessibilityServiceAlreadyRegisteredError\n\n\nclass BaseException(Exception):\n    \"\"\" base error for uiautomator2 \"\"\"\n\n## DeviceError\nclass DeviceError(BaseException): ...\nclass AdbShellError(DeviceError):...\nclass ConnectError(DeviceError):...\nclass HTTPError(DeviceError):...\nclass HTTPTimeoutError(HTTPError):...\nclass AdbBroadcastError(DeviceError):...\n\nclass UiAutomationError(DeviceError):...\nclass InputIMEError(DeviceError):...\n\nclass UiAutomationNotConnectedError(UiAutomationError):...    \nclass InjectPermissionError(UiAutomationError):... #开发者选项中: 模拟点击没有打开\nclass APKSignatureError(UiAutomationError):...\nclass LaunchUiAutomationError(UiAutomationError):...\nclass AccessibilityServiceAlreadyRegisteredError(UiAutomationError):...\n\n\n## RPCError\nclass RPCError(BaseException):\n    pass\n\nclass RPCUnknownError(RPCError):...\nclass RPCInvalidError(RPCError):...\nclass HierarchyEmptyError(RPCError):...\nclass RPCStackOverflowError(RPCError):...\n\n\nclass NormalError(RPCError):\n    pass\n\nclass XPathElementNotFoundError(NormalError):...\nclass SessionBrokenError(NormalError):... #only happens when app quit or crash\nclass UiObjectNotFoundError(NormalError):...\nclass AppNotFoundError(NormalError):..."
  },
  {
    "path": "uiautomator2/ext/__init__.py",
    "content": ""
  },
  {
    "path": "uiautomator2/ext/htmlreport/README.md",
    "content": "# HTMLReport for uiautomator2\n\nDemo code\n\n```python\n# coding: utf-8\n\nimport uiautomator2 as u2\nimport uiautomator2.ext.htmlreport as htmlreport\n\n\nu = u2.connect()\nhrp = htmlreport.HTMLReport(u)\n\n# take screenshot before each click\nhrp.patch_click()\n\nu.click(0.4, 0.6)\nu.click(0.4, 0.5)\nu(text=\"Github\").click() # will also record\n```\n\n## Screenshot\n![Alt](../../../docs/img/htmlreport.png)\n\n## LICENSE\nMIT"
  },
  {
    "path": "uiautomator2/ext/htmlreport/__init__.py",
    "content": "# coding: utf-8\n#\n\nfrom __future__ import print_function\n\nimport functools\nimport inspect\nimport json\nimport os\nimport shutil\nimport sys\nimport time\nimport types\n\nfrom PIL import ImageDraw\n\nimport uiautomator2\n\n\ndef mark_point(im, x, y):\n    \"\"\"\n    Mark position to show which point clicked\n\n    Args:\n        im: pillow.Image\n    \"\"\"\n    draw = ImageDraw.Draw(im)\n    w, h = im.size\n    draw.line((x, 0, x, h), fill='red', width=5)\n    draw.line((0, y, w, y), fill='red', width=5)\n    r = min(im.size) // 40\n    draw.ellipse((x - r, y - r, x + r, y + r), fill='red')\n    r = min(im.size) // 50\n    draw.ellipse((x - r, y - r, x + r, y + r), fill='white')\n    del draw\n    return im\n\n\nclass HTMLReport(object):\n    def __init__(self, driver, target_dir='report'):\n        self._driver = driver\n        self._target_dir = target_dir\n        self._steps = []\n        self._copy_assets()\n        self._flush()\n\n    def _copy_assets(self):\n        # py3 can use os.makedirs(dst, exist_ok=True), but py2 cannot\n        if not os.path.exists(self._target_dir):\n            os.makedirs(self._target_dir)\n\n        sdir = os.path.dirname(os.path.abspath(__file__))\n        for file in ['index.html', 'simplehttpserver.py', 'start.bat']:\n            src = os.path.join(sdir, 'assets', file)\n            dst = os.path.join(self._target_dir, file)\n            shutil.copyfile(src, dst)\n\n    def _record_screenshot(self, pos=None):\n        \"\"\"\n        Save screenshot and add record into record.json\n        \n        Example record data:\n        {\n            \"time\": \"2017/1/2 10:20:30\",\n            \"code\": \"d.click(100, 800)\",\n            \"screenshot\": \"imgs/demo.jpg\"\n        }\n        \"\"\"\n        im = self._driver.screenshot()\n        if pos:\n            x, y = pos\n            im = mark_point(im, x, y)\n            im.thumbnail((800, 800))\n        relpath = os.path.join('imgs', 'img-%d.jpg' % (time.time() * 1000))\n        abspath = os.path.join(self._target_dir, relpath)\n        dstdir = os.path.dirname(abspath)\n        if not os.path.exists(dstdir):\n            os.makedirs(dstdir)\n        im.save(abspath)\n        self._addtosteps(dict(screenshot=relpath))\n\n    def _addtosteps(self, data):\n        \"\"\"\n        Args:\n            data: dict used to save into record.json\n        \"\"\"\n        codelines = []\n        for stk in inspect.stack()[1:]:\n            filename = stk[1]\n            try:\n                filename = os.path.relpath(filename)\n            except ValueError:  # Windows: maybe on other driver, eg: C:/ F:/\n                continue\n            if filename.find(\"/site-packages/\") != -1:  # Linux\n                continue\n            if filename.startswith(\"..\"):  # only select files under curdir\n                continue\n            # --- stack ---\n            # 0: the frame object\n            # 1: the filename\n            # 2: the line number of the current line\n            # 3: the function name\n            # 4: a list of lines of context from the source code\n            # 5: the index of the current line within that list.\n            codeline = '%s:%d\\n  %s' % (filename, stk[2],\n                                        ''.join(stk[4] or []).strip())\n            codelines.append(codeline)\n        code = '\\n'.join(codelines)\n\n        steps = self._steps\n        base_data = {\n            'time': time.strftime(\"%H:%M:%S\"),\n            'code': code,\n        }\n        base_data.update(data)\n        steps.append(base_data)\n        self._flush()\n\n    def _flush(self):\n        record_file = os.path.join(self._target_dir, 'record.json')\n        with open(record_file, 'wb') as f:\n            f.write(json.dumps({'steps': self._steps}).encode('utf-8'))\n\n    def _patch_instance_func(self, obj, name, newfunc):\n        \"\"\" patch a.funcname to new func \"\"\"\n        oldfunc = getattr(obj, name)\n        print(\"mock\", oldfunc)\n        newfunc = functools.wraps(oldfunc)(newfunc)\n        newfunc.oldfunc = oldfunc\n        setattr(obj, name, types.MethodType(newfunc, obj))\n\n    def _patch_class_func(self, obj, funcname, newfunc):\n        \"\"\" patch A.funcname to new func \"\"\"\n        oldfunc = getattr(obj, funcname)\n        if hasattr(oldfunc, 'oldfunc'):\n            raise RuntimeError(\"function: %s.%s already patched before\" %\n                               (obj, funcname))\n        newfunc = functools.wraps(oldfunc)(newfunc)\n        newfunc.oldfunc = oldfunc\n        setattr(obj, funcname, newfunc)\n\n    def _unpatch_func(self, obj, funcname):\n        curfunc = getattr(obj, funcname)\n        if hasattr(curfunc, 'oldfunc'):\n            setattr(obj, funcname, curfunc.oldfunc)\n            return True\n\n    def patch_click(self):\n        \"\"\"\n        Record every click operation into report.\n        \"\"\"\n\n        def _mock_click(obj, x, y):\n            x, y = obj.pos_rel2abs(x, y)\n            self._record_screenshot((x, y))  # write image and record.json\n            return obj.click.oldfunc(obj, x, y)\n\n        def _mock_long_click(obj, x, y, duration=None):\n            x, y = obj.pos_rel2abs(x, y)\n            self._record_screenshot((x, y))  # write image and record.json\n            return obj.long_click.oldfunc(obj, x, y, duration)\n\n        self._patch_class_func(uiautomator2.Session, 'click', _mock_click)\n        self._patch_class_func(uiautomator2.Session, 'long_click',\n                               _mock_long_click)\n\n    def unpatch_click(self):\n        \"\"\"\n        Remove record for click operation\n        \"\"\"\n        self._unpatch_func(uiautomator2.Session, 'click')\n        self._unpatch_func(uiautomator2.Session, 'long_click')"
  },
  {
    "path": "uiautomator2/ext/htmlreport/assets/index.html",
    "content": "<!DOCTYPE html>\n<html>\n\n<head>\n  <meta charset=\"utf-8\">\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n  <!-- <link rel=\"icon\" href=\"/static/favicon.png?v=2\" type=\"image/x-icon\" /> -->\n  <title>U2 Report</title>\n  <!-- Bootstrap -->\n  <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/bootstrap/3.3.6/css/bootstrap.min.css\">\n  <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/@fancyapps/fancybox@3.3.5/dist/jquery.fancybox.min.css\">\n  <!-- \n  <link rel=\"stylesheet\" href=\"http://gohttp.nie.netease.com/qard-libs/libs/bootstrap/3.3.5/css/bootstrap.min.css\">\n  <link rel=\"stylesheet\" href=\"http://gohttp.nie.netease.com/qard-libs/libs/fancybox/2.1.5/jquery.fancybox.min.css\">\n  <link rel=\"stylesheet\" href=\"//cdn.bootcss.com/bootstrap/3.3.5/css/bootstrap-theme.min.css\">\n  -->\n  <style>\n    body {\n      padding-top: 70px;\n    }\n\n    .img-screenshot {\n      max-height: 400px;\n    }\n  </style>\n</head>\n\n<body>\n  <nav class=\"navbar navbar-default navbar-fixed-top\">\n    <div class=\"container-fluid\">\n      <div class=\"navbar-header\">\n        <button type=\"button\" class=\"navbar-toggle collapsed\" data-toggle=\"collapse\" data-target=\"#navbar\" aria-expanded=\"false\"\n          aria-controls=\"navbar\">\n          <span class=\"sr-only\">Toggle navigation</span>\n          <span class=\"icon-bar\"></span>\n          <span class=\"icon-bar\"></span>\n          <span class=\"icon-bar\"></span>\n        </button>\n        <a class=\"navbar-brand\" href=\"/\">测试过程记录</a>\n      </div>\n      <div id=\"navbar\" class=\"collapse navbar-collapse\">\n        <ul class=\"nav navbar-nav\">\n          <!-- <li class=\"dropdown\">\n            <a href=\"#\" class=\"dropdown-toggle\" data-toggle=\"dropdown\" role=\"button\" aria-expanded=\"false\">\n              列表 <span class=\"caret\"></span>\n            </a>\n            <ul class=\"dropdown-menu\" role=\"menu\">\n              <li v-for=\"step in steps\">\n                <a href=\"#{{step.time}}\">{{step.time}} {{step.action}}  {{step.description}}</a>\n              </li>\n            </ul>\n          </li> -->\n        </ul>\n      </div>\n    </div>\n  </nav>\n  <div class=\"container-fluid\" id=\"app\">\n    <div class=\"row\">\n      <div v-if=\"warning\" class=\"alert alert-warning alert-dismissible\" role=\"alert\">\n        <button type=\"button\" class=\"close\" data-dismiss=\"alert\" aria-label=\"Close\">\n          <span aria-hidden=\"true\">&times;</span>\n        </button>\n        <strong>Warning!</strong> {{warning}}\n      </div>\n\n      <div v-for=\"v, index in steps\">\n        <div class=\"col-md-4\">\n          <div style=\"margin-bottom: 20px; padding: 5px; background-color:aliceblue; position: relative\">\n            <div>\n              <b style=\"color: black;\">{{index+1}}</b> {{v.time}}</div>\n            <a :href=\"v.screenshot\" data-fancybox=\"gallery\">\n              <img style=\"margin-bottom: 5px\" class=\"img-thumbnail img-screenshot\" :src=\"v.screenshot\" style=\"max-height:200px\">\n            </a>\n            <pre style=\"text-overflow: ellipsis; overflow: hidden; font-size: 8px\">{{v.code}}</pre>\n          </div>\n        </div>\n      </div>\n    </div>\n    <blockquote class=\"code-font\">\n      <p>Make mobile test automated, free testers from endless work.</p>\n      <footer>Powered by\n        <a href=\"https://github.com/openatx/uiautomator2\">\n          <cite title=\"Shortcut\">atx uiautomator2</cite>\n        </a>\n      </footer>\n    </blockquote>\n  </div>\n</body>\n<script src=\"https://cdn.jsdelivr.net/jquery/1.11.3/jquery.min.js\"></script>\n<script src=\"https://cdn.jsdelivr.net/mousewheel/3.1.13/jquery.mousewheel.min.js\"></script>\n<script src=\"https://cdn.jsdelivr.net/npm/@fancyapps/fancybox@3.3.5/dist/jquery.fancybox.min.js\"></script>\n<script src=\"https://cdn.jsdelivr.net/bootstrap/3.3.6/js/bootstrap.min.js\"></script>\n<script src=\"https://cdn.jsdelivr.net/vue/2.3.2/vue.min.js\"></script>\n<!-- <script src=\"//cdn.rawgit.com/notifyjs/notifyjs/master/dist/notify.js\"></script> -->\n<script>\n  $.getJSON(\"record.json\")\n    .then(function (ret) {\n      console.log(ret)\n\n      // $(\".panel-heading\").on('click', function() {\n      //   $(this).next().toggle();\n      // })\n      ret.warning = null;\n      new Vue({\n        el: '#app',\n        data: ret,\n        computed: {}\n      })\n    }, function (ret) {\n      // Test data\n      new Vue({\n        el: '#app',\n        data: {\n          warning: \"GET /record.json failure，请双击 start.bat 浏览该界面\",\n          device: {\n            udid: \"AABBCCDDEEFF-1234567890\", // like serial but + mac and brand\n          },\n          steps: [{\n            time: \"2018/1/25 10:25:00\",\n            code: 'TEST: n(\"uictr_skill_normal_btn\").click_exists(2)',\n            screenshot: './pig.jpg',\n          }]\n        }\n      })\n    })\n</script>\n\n</html>"
  },
  {
    "path": "uiautomator2/ext/htmlreport/assets/simplehttpserver.py",
    "content": "#!/usr/bin/env python\n# coding: utf-8\n\nimport http.server as SimpleHTTPServer\nimport socket\nimport socketserver as SocketServer\nimport webbrowser\nfrom contextlib import closing\n\n\ndef is_port_avaiable(port):\n    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n    result = sock.connect_ex(('127.0.0.1', port))\n    return result != 0\n\n\ndef free_port():\n    if is_port_avaiable(11000):\n        return 11000\n\n    with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:\n        s.bind(('', 0))\n        return s.getsockname()[1]\n\n\ndef main():\n    PORT = free_port()\n    Handler = SimpleHTTPServer.SimpleHTTPRequestHandler\n    httpd = SocketServer.TCPServer((\"\", PORT), Handler)\n\n    # There is a bug that you have to refresh web page so you can see htmlreport\n    # Even I tried to use threading to delay webbrowser open tab\n    # but still need to refresh to let report show up.\n    # I guess this is SimpleHTTPServer bug\n    webbrowser.open('http://127.0.0.1:%d' % PORT, new=2)\n    print(\"serving at port\", PORT)\n    httpd.serve_forever(0.1)\n\n\nif __name__ == '__main__':\n    main()"
  },
  {
    "path": "uiautomator2/ext/htmlreport/assets/start.bat",
    "content": "python -u simplehttpserver.py"
  },
  {
    "path": "uiautomator2/ext/info/__init__.py",
    "content": "import atexit\nimport datetime\nimport json\nimport os\n\nfrom uiautomator2 import UIAutomatorServer\nfrom uiautomator2.ext.info import conf\n\n\nclass Info(object):\n    def __init__(self, driver, package_name=None):\n        self._driver = driver\n        self.output_dir = 'report/'\n        self.pkg_name = package_name\n        self.test_info = {}\n        atexit.register(self.write_info)\n\n    def read_file(self, filename):\n        try:\n            with open(self.output_dir + filename, 'r') as f:\n                return f.read()\n        except IOError as e:\n            print(os.strerror(e.errno))\n\n    def get_basic_info(self):\n        device_info = self._driver.device_info\n        app_info = self._driver.app_info(self.pkg_name)\n        # query for exact model info\n        if device_info['model'] in conf.phones:\n            device_info['model'] = conf.phones[device_info['model']]\n        self.test_info['basic_info'] = {'device_info': device_info, 'app_info': app_info}\n\n    def get_app_icon(self):\n        icon = self._driver.app_icon(self.pkg_name)\n        icon.save(self.output_dir + 'icon.png')\n\n    def get_record_info(self):\n        record = json.loads(self.read_file('record.json'))\n        steps = len(record['steps'])\n        start_time = datetime.datetime.strptime(record['steps'][0]['time'],\n                                                '%H:%M:%S')\n        end_time = datetime.datetime.strptime(\n            record['steps'][steps - 1]['time'], '%H:%M:%S')\n        total_time = end_time - start_time\n        self.test_info['record_info'] = {\n            'steps': steps,\n            'start_time': record['steps'][0]['time'],\n            'total_time': str(total_time)\n        }\n\n    def get_result_info(self):\n        log = self.read_file('log.txt')\n        trace_list = []\n        if log:\n            log = log.splitlines()\n            for i in range(len(log)):\n                if 'Traceback' in log[i]:\n                    new_trace = log[i]\n                    i += 1\n                    while 'File' in log[i]:\n                        new_trace += '\\n' + log[i]\n                        i += 1\n                    new_trace += '\\n' + log[i]\n                    trace_list.append(new_trace)\n        self.test_info['trace_info'] = {\n            'trace_count': len(trace_list),\n            'trace_list': trace_list\n        }\n\n    def start(self):\n        self.get_basic_info()\n        self.get_app_icon()\n\n    def write_info(self):\n        # self.get_basic_info()\n        self.get_record_info()\n        self.get_result_info()\n        with open(self.output_dir + 'info.json', 'wb') as f:\n            f.write(json.dumps(self.test_info))\n"
  },
  {
    "path": "uiautomator2/ext/info/conf.py",
    "content": "#! /usr/bin/env python\n# -*- coding:utf-8 -*-\n# Author: ljw\n\nphones = {\"1107\": \"OPPO 1107\",\n          \"15 Plus\": \"魅族 15 Plus\",\n          \"15\": \"魅族 15\",\n          \"1501-A02\": \"360奇酷 F4\",\n          \"1503-A01\": \"360奇酷 N4\",\n          \"1505-A01\": \"360奇酷 N4S\",\n          \"1505-A02\": \"360 N4S\",\n          \"1515-A01\": \"360奇酷 Q5\",\n          \"16 X\": \"魅族 16X\",\n          \"1603-A03\": \"360奇酷 N4A\",\n          \"1605-A01\": \"360奇酷 N5\",\n          \"1607-A01\": \"360奇酷 N5S\",\n          \"16th Plus\": \"魅族 16th Plus\",\n          \"16th\": \"魅族 16th\",\n          \"1707-A01\": \"360 N6\",\n          \"1801-A01\": \"360 N6 Pro\",\n          \"1803-A01\": \"360 N7 Lite\",\n          \"1807-A01\": \"360 N7\",\n          \"1809-A01\": \"360 N7 Pro\",\n          \"2013022\": \"红米 手机\",\n          \"2014011\": \"红米 1S\",\n          \"2014501\": \"红米 1S \",\n          \"2014813\": \"红米 2A\",\n          \"401SO\": \"索尼 Xperia Z3 \",\n          \"506SH\": \"夏普 AQUOS Xx3\",\n          \"831C\": \"HTC One M8\",\n          \"8676-A01\": \"酷派 大神 Note 3\",\n          \"8681-M02\": \"360奇酷 青春版\",\n          \"8692-A00\": \"360奇酷 旗舰版\",\n          \"A0001\": \"一加 1\",\n          \"A11\": \"OPPO A11\",\n          \"A31\": \"OPPO A31\",\n          \"ALE-UL00\": \"华为 P8 青春版\",\n          \"ALP-AL00\": \"华为 Mate 10\",\n          \"ANE-AL00\": \"华为 nova 3e\",\n          \"ARE-AL00\": \"华为 荣耀 8X Max\",\n          \"ASUS_T00J\": \"华硕 ZenFone 5\",\n          \"ASUS_X550\": \"华硕 飞马2 Plus\",\n          \"ASUS_Z00UD\": \"Zenfone Selfie\",\n          \"ASUS_Z012DE\": \"华硕 ZenFone 3\",\n          \"ASUS_Z016DA\": \"华硕 ZenFone 3\",\n          \"ASUS_Z01QD\": \"华硕 ROG\",\n          \"ATH-AL00\": \"华为 荣耀 7i\",\n          \"ATH-UL00\": \"华为 荣耀 7i\",\n          \"ATU-AL10\": \"华为 畅享 8e\",\n          \"AUM-AL20\": \"华为 荣耀畅玩 7A\",\n          \"BAC-TL00\": \"华为 nova 2 Plus\",\n          \"BKL-AL00\": \"华为 荣耀 V10\",\n          \"BKL-AL20\": \"华为 荣耀 V10\",\n          \"BLA-AL00\": \"华为 Mate 10 Pro\",\n          \"BLN-AL10\": \"华为 荣耀畅玩 6X\",\n          \"BLN-AL30\": \"华为 荣耀畅玩 6X\",\n          \"BLN-AL40\": \"华为 荣耀畅玩 6X\",\n          \"BLN-TL10\": \"华为 荣耀畅玩 6X\",\n          \"BND-AL10\": \"华为 荣耀畅玩 7X\",\n          \"BTV-W09\": \"华为 MediaPad M3\",\n          \"C105\": \"酷派 cool S1\",\n          \"C106\": \"酷派 cool1\",\n          \"C107-9\": \"酷派 cool 1C\",\n          \"C2105\": \"索尼 C2105\",\n          \"C8817D\": \"华为 荣耀 畅玩4\",\n          \"CAM-AL00\": \"华为 荣耀畅玩 5A\",\n          \"CAM-TL00\": \"华为 荣耀畅玩5A\",\n          \"CHE-TL00\": \"华为 荣耀 畅玩 4X\",\n          \"Che1-CL20\": \"华为 荣耀畅玩 4X\",\n          \"Che2-TL00M\": \"华为 荣耀 畅玩 4X\",\n          \"CHM-CL00\": \"华为 荣耀 4C 电信版\",\n          \"CHM-UL00\": \"华为 荣耀 4C\",\n          \"CLT-TL01\": \"华为 P20 Pro\",\n          \"COL-AL10\": \"华为 荣耀 10\",\n          \"Coolpad 8297-T01\": \"酷派 大神 F1\",\n          \"Coolpad 8297W\": \"酷派 大神 F1\",\n          \"Coolpad 8675\": \"酷派 大神 F2 移动版\",\n          \"Coolpad 8675-A\": \"酷派 大神 F2\",\n          \"Coolpad 8702D\": \"酷派 8720D\",\n          \"Coolpad 8705\": \"酷派 8705\",\n          \"Coolpad Y75\": \"酷派 锋尚\",\n          \"Coolpad Y803-9\": \"酷派 锋尚3\",\n          \"Coolpad Y80D\": \"酷派 锋尚\",\n          \"COR-AL00\": \"华为 荣耀Play\",\n          \"CUN-TL00\": \"华为 荣耀畅玩 5\",\n          \"d-02H\": \"华为 docomo dtab Compact\",\n          \"DE106\": \"坚果 R1\",\n          \"DIG-AL00\": \"华为 畅享 6S\",\n          \"DLI-AL10\": \"华为 荣耀畅玩 6A\",\n          \"DOOV L9\": \"朵唯 L9\",\n          \"DOOV S2\": \"朵唯 S2\",\n          \"DRA-AL00\": \"华为 畅享 8e \",\n          \"DUA-AL00\": \"华为 荣耀畅玩 7\",\n          \"DUK-AL20\": \"华为 荣耀 V9\",\n          \"E6\": \"金立 E6\",\n          \"E6533\": \"索尼 Xperia Z3\",\n          \"E6653\": \"索尼 Xperia Z\",\n          \"E6883\": \"索尼 Xperia Z5\",\n          \"E6mini\": \"金立 E6 mini\",\n          \"E7\": \"金立 E7\",\n          \"EDI-AL10\": \"华为 荣耀 Note8\",\n          \"EML-AL00\": \"华为 P20\",\n          \"EVA-AL00\": \"华为 P9\",\n          \"EVR-AL00\": \"华为 Mate 20 X\",\n          \"F-01H\": \"富士通 ARROWS \",\n          \"F-01J\": \"富士通 ARROWS\",\n          \"F-02G\": \"富士通 F-02G\",\n          \"F-02H\": \"富士通 ARROWS NX \",\n          \"F-04G\": \"富士通 ARROWS NX\",\n          \"F-04H\": \"ARROWS Tab\",\n          \"F100\": \"金立 F100\",\n          \"F100S\": \"金立 F100\",\n          \"F103\": \"金立 F103\",\n          \"F106\": \"金立 F106\",\n          \"F301\": \"金立 F301\",\n          \"F8332\": \"索尼 Xperia XZ\",\n          \"FIG-AL00\": \"华为 畅享 7S\",\n          \"FLA-AL10\": \"华为 畅享 8 Plus\",\n          \"FRD-AL00\": \"华为 荣耀 8\",\n          \"FRD-AL10\": \"华为 荣耀 8\",\n          \"FS8010\": \"夏普 AQUOS S2\",\n          \"G620S-UL00\": \"华为 荣耀 畅玩4\",\n          \"G8232\": \"索尼 Xperia XZs\",\n          \"GEM-703L\": \"华为 荣耀 X2\",\n          \"GIONEE F205L\": \"金立 F205\",\n          \"GIONEE F6L\": \"金立 F6\",\n          \"GIONEE GN5007\": \"金立 大金刚 2\",\n          \"GIONEE M7\": \"金立 M7\",\n          \"GIONEE S10\": \"金立 S10\",\n          \"GIONEE S11\": \"金立 S11\",\n          \"GIONEE S11S\": \"金立 S11s\",\n          \"GN151\": \"金立 GN151\",\n          \"GN5001S\": \"金立 金钢\",\n          \"GN5003\": \"金立 大金钢\",\n          \"GN5005\": \"金立 金钢 2\",\n          \"GN8002S\": \"金立 M6 Plus\",\n          \"GN8003\": \"金立 M6\",\n          \"GN9000\": \"金立 S5.5\",\n          \"GN9000L\": \"金立 S5.5L\",\n          \"GN9005\": \"金立 S5.1\",\n          \"GN9006\": \"金立 S7\",\n          \"GN9012\": \"金立 S6 Pro\",\n          \"GRA-A0\": \"酷派 酷玩 6C\",\n          \"GT-810\": \"掠夺者8 A5002\",\n          \"GT-I9060I\": \"三星 Galaxy Grand Neo Plus\",\n          \"GT-I9152\": \"三星 GT-I9152\",\n          \"GT-I9158P\": \"三星 Galaxy Mega\",\n          \"GT-I9208\": \"三星 GT-I9208\",\n          \"GT-I9300\": \"三星 Galaxy S3\",\n          \"GT-I9500\": \"三星 GalaxyS4\",\n          \"GT-I9507V\": \"三星 Galaxy S4\",\n          \"GT-I9508\": \"三星 Galaxy S4\",\n          \"GT-N7100\": \"三星 Galaxy Note 2\",\n          \"GT-N7108\": \"三星 Galaxy Note 2\",\n          \"H30-T00\": \"华为 荣耀3C\",\n          \"H30-T10\": \"华为 荣耀3C\",\n          \"H30-U10\": \"华为 荣耀3C\",\n          \"H60-L01\": \"华为 荣耀 6\",\n          \"H60-L03\": \"华为 荣耀 6\",\n          \"H8296\": \"索尼 Xperia XZ2\",\n          \"Hisense A2\": \"海信 A2\",\n          \"Hisense E76\": \"海信 E76\",\n          \"HLA NOTE1-L\": \"红辣椒 Note\",\n          \"HM 1S\": \"红米 1S\",\n          \"HM 2A\": \"红米 2A\",\n          \"HM NOTE 1LTE\": \"红米 Note\",\n          \"HM NOTE 1S\": \"红米 Note\",\n          \"HM NOTE 1TD\": \"红米 Note\",\n          \"HTC 2Q4D200\": \"HTC U11+\",\n          \"HTC 2Q4R400\": \"HTC U11 EYEs\",\n          \"HTC 2Q55300\": \"HTC U12+\",\n          \"HTC 802w\": \"HTC One M7\",\n          \"HTC 8088\": \"HTC One max\",\n          \"HTC 9088\": \"HTC Butterfly S\",\n          \"HTC A9w\": \"HTC One A9\",\n          \"HTC D10w\": \"HTC Desire 10 Pro\",\n          \"HTC D816w\": \"HTC Desire 816\",\n          \"HTC D820u\": \"HTC D820u\",\n          \"HTC D830u\": \"HTC Desire 830\",\n          \"HTC Desire EYE\": \"HTC Desire Eye\",\n          \"HTC M10u\": \"HTC 10\",\n          \"HTC M8t\": \"HTC One E8\",\n          \"HTC One A9\": \"HTC One A9\",\n          \"HTC One M9PLUS\": \"HTC M9 台版\",\n          \"HTC One X9 dual sim\": \"HTC One X9\",\n          \"HTC U Ultra\": \"HTC U Ultra 港版\",\n          \"HTC U-1w\": \"HTC U Ultra\",\n          \"HTC_M10h\": \"HTC 10\",\n          \"HTC_M9u\": \"HTC One M9\",\n          \"HUAWEI CAZ-AL10\": \"华为 nova\",\n          \"HUAWEI CAZ-TL20\": \"华为 nova\",\n          \"HUAWEI G606-T00\": \"华为 G606-T00\",\n          \"HUAWEI G610-U00\": \"华为 G610\",\n          \"HUAWEI G7-TL00\": \"华为 G7\",\n          \"HUAWEI G700-T00\": \"华为 G700-T00\",\n          \"HUAWEI G700-U00\": \"华为 G700-U00\",\n          \"HUAWEI G750-T01\": \"华为 荣耀3X\",\n          \"HUAWEI GRA-CL10\": \"华为 P8 高配版\",\n          \"HUAWEI GRA-TL00\": \"华为 P8\",\n          \"HUAWEI GRA-UL00\": \"华为 P8\",\n          \"HUAWEI M2-801W\": \"华为 MediaPad\",\n          \"HUAWEI M2-A01W\": \"华为 MediaPad\",\n          \"HUAWEI MLA-AL00\": \"华为 麦芒 5\",\n          \"HUAWEI MLA-TL10\": \"华为 G9 Plus\",\n          \"HUAWEI MLA-UL00\": \"华为 G9 Plus\",\n          \"HUAWEI MT2-L01\": \"华为 Mate 2\",\n          \"HUAWEI MT7-TL00\": \"华为 Mate 7\",\n          \"HUAWEI MT7-TL10\": \"华为 Mate 7\",\n          \"HUAWEI NXT-AL10\": \"华为 Mate 8\",\n          \"HUAWEI NXT-DL00\": \"华为 Mate 8\",\n          \"HUAWEI P6 S-U06\": \"华为 P6 S\",\n          \"HUAWEI P6-T00\": \"华为 P6\",\n          \"HUAWEI P7-L00\": \"华为 P7\",\n          \"HUAWEI P7-L05\": \"华为 P7\",\n          \"HUAWEI P7-L07\": \"华为 P7\",\n          \"HUAWEI P8max\": \"华为 P8max\",\n          \"HUAWEI RIO-AL00\": \"华为 麦芒 4\",\n          \"HUAWEI RIO-CL00\": \"华为 麦芒 4\",\n          \"HUAWEI RIO-TL00\": \"华为 G7 Plus\",\n          \"HUAWEI TAG-AL00\": \"华为 畅享 5S\",\n          \"HUAWEI TAG-TL00\": \"华为 畅享 5S\",\n          \"HUAWEI TIT-AL00\": \"华为 畅享 5\",\n          \"HUAWEI VNS-AL00\": \"华为 G9\",\n          \"HUAWEI VNS-L21\": \"华为 P9 Lite\",\n          \"HUAWEI VNS-TL00\": \"华为 G9\",\n          \"HWI-AL00\": \"华为 nova 2S\",\n          \"HWT31\": \"华为 au Qua tab\",\n          \"IM-A870L\": \"泛泰 VEGA\",\n          \"INE-AL00\": \"华为 nova 3i\",\n          \"JMM-AL00\": \"华为 荣耀 V9 Play\",\n          \"JSN-AL00a\": \"华为 荣耀 8X\",\n          \"K011\": \"华硕 Memo Pad\",\n          \"KIW-AL10\": \"华为 荣耀畅玩 5X\",\n          \"KIW-TL00H\": \"华为 荣耀畅玩 5X\",\n          \"KNT-AL10\": \"华为 荣耀 V8\",\n          \"KNT-TL10\": \"华为 荣耀 V8\",\n          \"KOB-W09\": \"华为 荣耀畅玩\",\n          \"KYT31\": \"KYOCERA au Qua tab\",\n          \"L36h\": \"索尼 Z\",\n          \"L39h\": \"索尼 Xperia Z1\",\n          \"L50w\": \"索尼 Xperia Z2\",\n          \"LA7-L\": \"小辣椒 7\",\n          \"LDN-AL00\": \"华为 畅享 8\",\n          \"Le X507\": \"乐视 乐1s\",\n          \"Le X620\": \"乐视 乐2\",\n          \"Le X625\": \"乐视 乐2 Pro\",\n          \"Le X820\": \"乐视 乐Max 2\",\n          \"Lenovo A788t\": \"联想 A788t\",\n          \"Lenovo A808t\": \"联想 黄金斗士A8\",\n          \"Lenovo A808t-i\": \"联想 黄金斗士A8\",\n          \"Lenovo A850\": \"联想 A850\",\n          \"Lenovo K30-T\": \"联想 乐檬 K3\",\n          \"Lenovo K900\": \"联想 K900\",\n          \"Lenovo K920\": \"联想 K920\",\n          \"Lenovo L78011\": \"联想 Z5\",\n          \"Lenovo P2c72\": \"联想 VIBE P2\",\n          \"Lenovo P70-t\": \"联想 P70t\",\n          \"Lenovo PB2-690N\": \"联想 Phab2 Pro\",\n          \"Lenovo S60-t\": \"联想 S60t\",\n          \"Lenovo S850t\": \"联想 S850t\",\n          \"Lenovo S868t\": \"联想 S868t\",\n          \"Lenovo S90-t\": \"联想 笋尖S90\",\n          \"Lenovo X2-TO\": \"联想 VIBE X2\",\n          \"Letv X500\": \"乐视 乐1s\",\n          \"LEX720\": \"乐视 乐Pro3\",\n          \"LG-D486\": \"LG D486\",\n          \"LG-D855\": \"LG G3\",\n          \"LG-E985T\": \"LG E985T\",\n          \"LG-H818\": \"LG G4\",\n          \"LG-H860\": \"LG G5\",\n          \"LG-H968\": \"LG V10\",\n          \"LG-M250\": \"LG K10 2017\",\n          \"LGMS210\": \"LG Aristo MS210\",\n          \"LGT31\": \"LGT 31\",\n          \"LGV32\": \"LG G4\",\n          \"LLD-AL00\": \"华为 荣耀 9 青春版\",\n          \"LLD-AL20\": \"华为 荣耀 9i\",\n          \"LND-AL30\": \"华为 荣耀畅玩 7C\",\n          \"LON-AL00\": \"华为 Mate 9 Pro\",\n          \"LYA-AL00\": \"华为 Mate 20 Pro\",\n          \"M040\": \"魅族 MX2\",\n          \"M1 E\": \"魅族 魅蓝 E\",\n          \"m1 metal\": \"魅族 魅蓝 Metal\",\n          \"m1 note\": \"魅族 魅蓝 Note\",\n          \"m1\": \"魅族 魅蓝\",\n          \"M15\": \"魅族 M15\",\n          \"M1813\": \"魅族 V8 高配版\",\n          \"M1816\": \"魅族 V8 标准版\",\n          \"M1852\": \"魅族 X8\",\n          \"M2 E\": \"魅族 魅蓝 E2\",\n          \"m2 note\": \"魅族 魅蓝 Note 2\",\n          \"m2\": \"魅族 魅蓝 2\",\n          \"M3 Max\": \"魅族 魅蓝 Max\",\n          \"m3 note\": \"魅族 魅蓝 Note 3\",\n          \"M351\": \"魅族 MX3\",\n          \"M3s\": \"魅族 魅蓝 3S\",\n          \"M3X\": \"魅族 魅蓝 X\",\n          \"M463C\": \"魅族 魅蓝 Note 电信版\",\n          \"M5 Note\": \"魅族 魅蓝 Note 5\",\n          \"M5\": \"魅族 魅蓝 5\",\n          \"M5s\": \"魅族 魅蓝 5S\",\n          \"M6 Note\": \"魅族 魅蓝 Note 6\",\n          \"M6\": \"魅族 魅蓝 6\",\n          \"M623C\": \"中国移动 A1\",\n          \"M651CY\": \"中国移动 A3\",\n          \"M760\": \"中国移动 A4s\",\n          \"Meitu M4\": \"美图 M4\",\n          \"Meizu 6T\": \"魅族 魅蓝 6T\",\n          \"MEIZU E3\": \"魅族 魅蓝 E3\",\n          \"Meizu S6\": \"魅族 魅蓝 S6\",\n          \"MHA-AL00\": \"华为 Mate 9\",\n          \"MHA-L29\": \"华为 Mate 9 国际版\",\n          \"MI 2\": \"小米 2\",\n          \"MI 2A\": \"小米 2A\",\n          \"MI 3\": \"小米 3 移动版\",\n          \"MI 3C\": \"小米 3 电信版\",\n          \"MI 3W\": \"小米 3 联通版\",\n          \"MI 4LTE\": \"小米 4 \",\n          \"MI 4S\": \"小米 4S\",\n          \"MI 4W\": \"小米 4 联通版\",\n          \"MI 5\": \"小米 5\",\n          \"MI 5C\": \"小米 5C\",\n          \"MI 5s Plus\": \"小米 5S Plus\",\n          \"MI 5s\": \"小米 5S\",\n          \"MI 5X\": \"小米 5X\",\n          \"MI 6\": \"小米 6\",\n          \"MI 6X\": \"小米 6X\",\n          \"MI 8 Explorer Edition\": \"小米 8 透明探索版\",\n          \"MI 8 Lite\": \"小米 8 青春版\",\n          \"MI 8 SE\": \"小米 8 SE\",\n          \"MI 8 UD\": \"小米 8\",\n          \"MI 8\": \"小米 8\",\n          \"Mi A1\": \"小米 A1\",\n          \"MI MAX 2\": \"小米 MAX 2\",\n          \"MI MAX 3\": \"小米 Max 3\",\n          \"MI MAX\": \"小米 Max\",\n          \"Mi Note 2\": \"小米 Note 2\",\n          \"Mi Note 3\": \"小米 Note 3\",\n          \"MI NOTE LTE\": \"小米 Note\",\n          \"MI NOTE Pro\": \"小米 Note 顶配版\",\n          \"MI PAD 2\": \"小米 平板2\",\n          \"MI PAD 3\": \"小米 平板3\",\n          \"MI PAD 4 PLUS\": \"小米 平板4 Plus\",\n          \"MI PAD 4\": \"小米 平板4\",\n          \"MI PAD\": \"小米 平板\",\n          \"Mi-4c\": \"小米 4C 标准版\",\n          \"MI-ONE Plus\": \"小米 1\",\n          \"MIX 2\": \"小米 MIX 2\",\n          \"MIX 2S\": \"小米 MIX 2S\",\n          \"MIX 3\": \"小米 MIX 3\",\n          \"MIX\": \"小米 MIX\",\n          \"Moto E (4)\": \"Moto E\",\n          \"Moto G (5) Plus\": \"Moto G5 Plus\",\n          \"Moto G (5)\": \"Moto G5\",\n          \"Moto X Pro\": \"Moto X Pro\",\n          \"MP1503\": \"美图 M6\",\n          \"MX4 Pro\": \"魅族 MX4 Pro\",\n          \"MX4\": \"魅族 MX4\",\n          \"MX5\": \"魅族 MX5\",\n          \"MX6\": \"魅族 MX6\",\n          \"MYA-AL10\": \"华为 荣耀畅玩 6\",\n          \"MYA-L22\": \"华为 Y5\",\n          \"N1\": \"诺基亚 N1\",\n          \"N1T\": \"OPPO N1T\",\n          \"N5207\": \"OPPO N3\",\n          \"NCE-AL00\": \"华为 畅享 6\",\n          \"NCE-AL10\": \"华为 畅享 6\",\n          \"NCE-TL10\": \"华为 畅享 6\",\n          \"NEM-AL10\": \"华为 荣耀 畅玩5C\",\n          \"NEM-TL00\": \"华为 荣耀 畅玩5C\",\n          \"NEM-TL00H\": \"华为 荣耀 畅玩5C\",\n          \"NEO-AL00\": \"华为 Mate RS\",\n          \"Nexus 4\": \"Google Nexus 4\",\n          \"Nexus 5\": \"Google Nexus 5\",\n          \"Nexus 5X\": \"Google Nexus 5X\",\n          \"Nexus 6\": \"Google Nexus 6\",\n          \"Nexus 6P\": \"Google Nexus 6P\",\n          \"Nexus 9\": \"Google Nexus 9\",\n          \"Nokia 7 plus\": \"诺基亚 7 Plus\",\n          \"Nokia 8 Sirocco\": \"诺基亚 8 Sirocco\",\n          \"Nokia X5\": \"诺基亚 X5\",\n          \"Nokia X6\": \"诺基亚 X6\",\n          \"NTS-AL00\": \"华为 荣耀 Magic\",\n          \"NX403A\": \"努比亚 Z5S mini\",\n          \"NX508J\": \"努比亚 Z9\",\n          \"NX513J\": \"努比亚 My\",\n          \"NX529J\": \"努比亚 Z11 mini\",\n          \"NX531J\": \"努比亚 Z11\",\n          \"NX563J\": \"努比亚 Z17\",\n          \"NX569J\": \"努比亚 Z17 mini \",\n          \"NX573J\": \"努比亚 M2\",\n          \"NX575J\": \"努比亚 N2\",\n          \"NX595J\": \"努比亚 Z17S\",\n          \"NX609J\": \"努比亚 红魔手机\",\n          \"NXT-AL10\": \"华为 Mate 8\",\n          \"OC105\": \"锤子 坚果 3\",\n          \"OD103\": \"锤子 坚果 Pro\",\n          \"OE106\": \"锤子 坚果 Pro 2S\",\n          \"ONE A2001\": \"一加 2\",\n          \"ONEPLUS A3000\": \"一加 3\",\n          \"ONEPLUS A3010\": \"一加 3T\",\n          \"ONEPLUS A5000\": \"一加 5\",\n          \"ONEPLUS A5010\": \"一加 5T\",\n          \"ONEPLUS A6000\": \"一加 6\",\n          \"OPPO A30\": \"OPPO A30\",\n          \"OPPO A33\": \"OPPO A33\",\n          \"OPPO A33m\": \"OPPO A33\",\n          \"OPPO A37m\": \"OPPO A37\",\n          \"OPPO A53\": \"OPPO A53\",\n          \"OPPO A53m\": \"OPPO A53\",\n          \"OPPO A57\": \"OPPO A57\",\n          \"OPPO A59m\": \"OPPO A59\",\n          \"OPPO A59s\": \"OPPO A59s\",\n          \"OPPO A73\": \"OPPO A73\",\n          \"OPPO A77\": \"OPPO A77\",\n          \"OPPO A79\": \"OPPO A79\",\n          \"OPPO A83\": \"OPPO A83\",\n          \"OPPO R11 Plus\": \"OPPO R11 Plus\",\n          \"OPPO R11 Plusk\": \"OPPO R11 Plus\",\n          \"OPPO R11\": \"OPPO R11\",\n          \"OPPO R11s Plus\": \"OPPO R11s Plus\",\n          \"OPPO R11s\": \"OPPO R11s\",\n          \"OPPO R11st\": \"OPPO R11s\",\n          \"OPPO R11t\": \"OPPO R11\",\n          \"OPPO R7\": \"OPPO R7\",\n          \"OPPO R7s\": \"OPPO R7s \",\n          \"OPPO R7sm\": \"OPPO R7s 全网通\",\n          \"OPPO R9 Plustm A\": \"OPPO R9 Plus\",\n          \"OPPO R9m\": \"OPPO R9\",\n          \"OPPO R9s Plus\": \"OPPO R9s Plus\",\n          \"OPPO R9s\": \"OPPO R9s\",\n          \"OPPO R9sk\": \"OPPO R9s\",\n          \"OPPO R9tm\": \"OPPO R9\",\n          \"OS105\": \"锤子 坚果 Pro 2\",\n          \"PAAM00\": \"OPPO R15\",\n          \"PADM00\": \"OPPO A3\",\n          \"PAFM00\": \"OPPO Find X\",\n          \"PAR-AL00\": \"华为 nova 3\",\n          \"PBAM00\": \"OPPO A5\",\n          \"PBEM00\": \"OPPO R17\",\n          \"PE-CL00\": \"华为 荣耀 6 Plus\",\n          \"PE-TL10\": \"华为 荣耀 6 Plus\",\n          \"PH-1\": \"Essential Phone\",\n          \"PIC-AL00\": \"华为 nova 2\",\n          \"Pixel 2 XL\": \"Google Pixel 2 XL\",\n          \"Pixel XL\": \"Google Pixel XL\",\n          \"Pixel\": \"Google Pixel\",\n          \"PLAYER\": \"小辣椒 Player\",\n          \"PLK-CL00\": \"华为 荣耀 7\",\n          \"PLK-TL01H\": \"华为 荣耀 7\",\n          \"PLK-UL00\": \"华为 荣耀 7\",\n          \"POCOPHONE F1\": \"POCOPHONE F1\",\n          \"PP5600\": \"PPTV M1\",\n          \"PRA-AL00\": \"华为 荣耀 8 青春版\",\n          \"PRA-AL00X\": \"华为 荣耀 8 青春版\",\n          \"PRO 5\": \"魅族 Pro 5\",\n          \"PRO 6 Plus\": \"魅族 Pro 6 Plus\",\n          \"PRO 6\": \"魅族 Pro 6\",\n          \"PRO 6s\": \"魅族 Pro 6s\",\n          \"PRO 7 Plus\": \"魅族 Pro 7 Plus\",\n          \"PRO 7-S\": \"魅族 Pro 7\",\n          \"R2017\": \"OPPO R2017\",\n          \"R6007\": \"OPPO R6007\",\n          \"R7007\": \"OPPO R3\",\n          \"R7c\": \"OPPO R7 电信版\",\n          \"R7Plus\": \"OPPO R7 Plus 移动版\",\n          \"R7Plusm\": \"OPPO R7 Plus 全网通\",\n          \"R8107\": \"OPPO R8107\",\n          \"R819T\": \"OPPO R819T\",\n          \"R8207\": \"OPPO R1C\",\n          \"R821T\": \"OPPO R821T\",\n          \"R831S\": \"OPPO R831s\",\n          \"R831T\": \"OPPO R831T\",\n          \"ramos MOS 1\": \"蓝魔 MOS1\",\n          \"ramos MOS1max\": \"蓝魔 MOS1 MAX\",\n          \"Redmi 3\": \"红米 3\",\n          \"Redmi 3S\": \"红米 3S\",\n          \"Redmi 3X\": \"红米 3X\",\n          \"Redmi 4\": \"红米 4\",\n          \"Redmi 4A\": \"红米 4A\",\n          \"Redmi 4X\": \"红米 4X\",\n          \"Redmi 5 Plus\": \"红米 5 Plus\",\n          \"Redmi 5\": \"红米 5\",\n          \"Redmi 5A\": \"红米 5A\",\n          \"Redmi 6 Pro\": \"红米 6 Pro\",\n          \"Redmi 6A\": \"红米 6A\",\n          \"Redmi Note 2\": \"红米 Note 2\",\n          \"Redmi Note 3\": \"红米 Note 3\",\n          \"Redmi Note 4\": \"红米 Note 4\",\n          \"Redmi Note 4X\": \"红米 Note 4X\",\n          \"Redmi Note 5\": \"红米 Note 5\",\n          \"Redmi Note 5A\": \"红米 Note 5A\",\n          \"Redmi Pro\": \"红米 Pro\",\n          \"Redmi S2\": \"红米 S2\",\n          \"RNE-AL00\": \"华为 麦芒 6\",\n          \"RVL-AL09\": \"华为 荣耀 Note10\",\n          \"S39h\": \"索尼 Xperia C\",\n          \"S9\": \"金立 S9\",\n          \"SAMSUNG-SM-G930A\": \"三星 Galaxy S7 美版\",\n          \"SC-03J\": \"三星 Galaxy S8\",\n          \"SC-04E\": \"三星 Galaxy S4\",\n          \"SC-04F\": \"三星 Galaxy S5\",\n          \"SC-04G\": \"三星 Galaxy S6 Edg\",\n          \"SC-04J\": \"三星 Galaxy Feel\",\n          \"SC-05G\": \"三星 Galaxy S6\",\n          \"SCH-I939I\": \"三星 Galaxy S3 Neo+\",\n          \"SCL-AL00\": \"华为 荣耀 4A\",\n          \"SCV31\": \"三星 Galaxy S6 Edge\",\n          \"SCV35\": \"三星 Galaxy S8+\",\n          \"SCV36\": \"三星 Galaxy S8\",\n          \"SD4930UR\": \"Amzon Fire Phone\",\n          \"SGH-N075T\": \"三星 Galaxy J\",\n          \"SKR-A0\": \"黑鲨 游戏手机\",\n          \"SLA-AL00\": \"华为 畅享 7\",\n          \"SM-A3000\": \"三星 Galaxy A3\",\n          \"SM-A5000\": \"三星 Galaxy A5\",\n          \"SM-A5100\": \"三星 Galaxy A5\",\n          \"SM-A520S\": \"三星 Galaxy A5\",\n          \"SM-A7000\": \"三星 Galaxy A7\",\n          \"SM-C5010\": \"三星 Galaxy C5 Pro\",\n          \"SM-C7000\": \"三星 Galaxy C7\",\n          \"SM-C7010\": \"三星 Galaxy C7 Pro\",\n          \"SM-C7100\": \"三星 Galaxy C8\",\n          \"SM-C9000\": \"三星 Galaxy C9 Pro\",\n          \"SM-E7000\": \"三星 Galaxy E7\",\n          \"SM-G5309W\": \"三星 Galaxy GRAND\",\n          \"SM-G530H\": \"三星 Galaxy Grand Prime \",\n          \"SM-G532F\": \"三星 Grand Prime Plus\",\n          \"SM-G5500\": \"三星 Galaxy On5\",\n          \"SM-G6000\": \"三星 Galaxy On7\",\n          \"SM-G7108V\": \"三星 Galaxy Grand 2\",\n          \"SM-G8508S\": \"三星 Galaxy Alpha\",\n          \"SM-G8750\": \"三星 Galaxy S \",\n          \"SM-G9006W\": \"三星 Galaxy S5\",\n          \"SM-G9008V\": \"三星 Galaxy S5\",\n          \"SM-G900V\": \"三星 Galaxy S5\",\n          \"SM-G910S\": \"三星 Galaxy Round\",\n          \"SM-G9200\": \"三星 Galaxy S6\",\n          \"SM-G920F\": \"三星 Galaxy S6\",\n          \"SM-G920V\": \"三星 Galaxy S6\",\n          \"SM-G9250\": \"三星 Galaxy S6 Edge\",\n          \"SM-G925V\": \"三星 Galaxy S6 Edge\",\n          \"SM-G9280\": \"三星 Galaxy S6 Edge+\",\n          \"SM-G9300\": \"三星 Galaxy S7\",\n          \"SM-G9308\": \"三星 Galaxy S7\",\n          \"SM-G930F\": \"三星 Galaxy S7\",\n          \"SM-G9350\": \"三星 Galaxy S7 Edge\",\n          \"SM-G935F\": \"三星 Galaxy S7 Edge\",\n          \"SM-G935V\": \"三星 Galaxy S7 Edge\",\n          \"SM-G9500\": \"三星 Galaxy S8\",\n          \"SM-G950F\": \"三星 Galaxy S8\",\n          \"SM-G9550\": \"三星 Galaxy S8+\",\n          \"SM-G955N\": \"三星 Galaxy S8+\",\n          \"SM-G9600\": \"三星 Galaxy S9\",\n          \"SM-G960N\": \"三星 Galaxy S9 \",\n          \"SM-G960U\": \"三星 Galaxy S9\",\n          \"SM-G965N\": \"三星 Galaxy S9+\",\n          \"SM-G965U\": \"三星 Galaxy S9+\",\n          \"SM-J5008\": \"三星 Galaxy J5\",\n          \"SM-J7008\": \"三星 Galaxy J7\",\n          \"SM-J701F\": \"三星 Galaxy J7\",\n          \"SM-J7109\": \"三星 Galaxy J7\",\n          \"SM-J730GM\": \"三星 Galaxy J7 Pro\",\n          \"SM-N9002\": \"三星 Galaxy Note 3\",\n          \"SM-N9008V\": \"三星 Galaxy Note 3\",\n          \"SM-N900A\": \"三星 Galaxy Note 3\",\n          \"SM-N9100\": \"三星 Galaxy Note 4\",\n          \"SM-N910U\": \"三星 Galaxy Note 4\",\n          \"SM-N9200\": \"三星 Galaxy Note 5\",\n          \"SM-N9208\": \"三星 Galaxy Note 5\",\n          \"SM-N920K\": \"三星 Galaxy Note 5\",\n          \"SM-N9500\": \"三星 Galaxy Note 8\",\n          \"SM-N950N\": \"三星 Galaxy Note 8\",\n          \"SM-N950U1\": \"三星 Galaxy Note 8\",\n          \"SM-N9600\": \"三星 Galaxy Note 9\",\n          \"SM-P601\": \"三星 Galaxy Note 10.1\",\n          \"SM-T110\": \"三星 Galaxy Tab3\",\n          \"SM-T113\": \"三星 Tab E Lite\",\n          \"SM-T310\": \"三星 GALAXY Tab3\",\n          \"SM-T560\": \"三星 Galaxy Tab E\",\n          \"SM-T580\": \"三星 Galaxy Tab A\",\n          \"SM-T817V\": \"三星 Galaxy Tab S2\",\n          \"SM701\": \"锤子 T1\",\n          \"SM901\": \"锤子 M1\",\n          \"SM919\": \"锤子 M1L\",\n          \"SNE-AL00\": \"华为 麦芒 7\",\n          \"SO-03G\": \"索尼 Xperia Z4\",\n          \"SO-04H\": \"索尼 Xperia X Performance\",\n          \"SO-04J\": \"索尼 Xperia XZ Premium\",\n          \"SOV31\": \"索尼 Xperia Z4\",\n          \"SOV35\": \"索尼 Xperia XZs\",\n          \"STF-AL00\": \"华为 荣耀 9\",\n          \"STF-AL10\": \"华为 荣耀 9\",\n          \"TA-1000\": \"诺基亚 6\",\n          \"TA-1041\": \"诺基亚 7\",\n          \"TA-1052\": \"诺基亚 8\",\n          \"TCL 520\": \"TCL 520\",\n          \"TCL 750\": \"TCL 750\",\n          \"TRT-AL00\": \"华为 畅享 7 Plus\",\n          \"U10\": \"魅族 魅蓝 U10\",\n          \"V1732A\": \"vivo Y81s\",\n          \"V1809A\": \"vivo X23\",\n          \"V1813A\": \"vivo Y97\",\n          \"V182\": \"金立 V182\",\n          \"V183\": \"金立 V183\",\n          \"V185\": \"金立 V185\",\n          \"V188S\": \"金立 V188S\",\n          \"V8\": \"天语 V8\",\n          \"VCR-A0\": \"酷派 酷玩 6\",\n          \"VIE-AL10\": \"华为 P9 Plus\",\n          \"Vivo 5R\": \"BLU vivo 5R\",\n          \"vivo NEX A\": \"vivo NEX 标准版\",\n          \"vivo NEX S\": \"vivo NEX 旗舰版\",\n          \"vivo V3\": \"vivo V3\",\n          \"vivo V3M A\": \"vivo V3M\",\n          \"vivo V3Max A\": \"vivo V3Max\",\n          \"vivo V3Max\": \"vivo V3Max\",\n          \"vivo X20A\": \"vivo X20\",\n          \"vivo X20Plus A\": \"vivo X20 Plus\",\n          \"vivo X20Plus UD\": \"vivo X20 PlusUD\",\n          \"vivo X20Plus\": \"vivo X20 Plus\",\n          \"vivo X21A\": \"vivo X21\",\n          \"vivo X21i A\": \"vivo X21i\",\n          \"vivo X21UD A\": \"vivo X21UD\",\n          \"vivo X3L\": \"vivo X3L\",\n          \"vivo X3t\": \"vivo X3T\",\n          \"vivo X5L\": \"vivo X5L\",\n          \"vivo X5M\": \"vivo X5M\",\n          \"vivo X5Max V\": \"vivo X5 Max V\",\n          \"vivo X5Max+\": \"vivo X5Max+\",\n          \"vivo X5Pro D\": \"vivo X5Pro\",\n          \"vivo X5Pro V\": \"vivo X5Pro V\",\n          \"vivo X5S L\": \"vivo X5 SL\",\n          \"vivo X6A\": \"vivo X6A\",\n          \"vivo X6D\": \"vivo X6D\",\n          \"vivo X6Plus A\": \"vivo X6 Plus A\",\n          \"vivo X6Plus D\": \"vivo X6 Plus\",\n          \"vivo X6Plus L\": \"vivo X6 Plus\",\n          \"vivo X6S A\": \"vivo X6S\",\n          \"vivo X6SPlus D\": \"vivo X6S Plus\",\n          \"vivo X7\": \"vivo X7\",\n          \"vivo X7Plus\": \"vivo X7 Plus\",\n          \"vivo X9\": \"vivo X9\",\n          \"vivo X9i\": \"vivo X9\",\n          \"vivo X9Plus\": \"vivo X9 Plus\",\n          \"vivo X9s Plus\": \"vivo X9s Plus\",\n          \"vivo X9s\": \"vivo X9s\",\n          \"vivo Xplay3S\": \"vivo Xplay 3S\",\n          \"vivo Xplay5A\": \"vivo Xplay5\",\n          \"vivo Xplay6\": \"vivo Xplay6\",\n          \"vivo Y11i T\": \"vivo Y11iT\",\n          \"vivo Y13L\": \"vivo Y13L\",\n          \"vivo Y23L\": \"vivo Y23L\",\n          \"vivo Y27\": \"vivo Y27\",\n          \"vivo Y29L\": \"vivo Y29L\",\n          \"vivo Y31A\": \"vivo Y31A\",\n          \"vivo Y33\": \"vivo Y33\",\n          \"vivo Y35\": \"vivo Y35L\",\n          \"vivo Y35A\": \"vivo Y35A\",\n          \"vivo Y35L\": \"vivo Y35L\",\n          \"vivo Y37\": \"vivo Y37\",\n          \"vivo Y51A\": \"vivo Y51A\",\n          \"vivo Y53\": \"vivo Y53\",\n          \"vivo Y55\": \"vivo Y55A\",\n          \"vivo Y66\": \"vivo Y66\",\n          \"vivo Y67\": \"vivo Y67\",\n          \"vivo Y67A\": \"vivo Y67\",\n          \"vivo Y69A\": \"vivo Y69\",\n          \"vivo Y71A\": \"vivo Y71\",\n          \"vivo Y75\": \"vivo Y75\",\n          \"vivo Y75s\": \"vivo Y75s\",\n          \"vivo Y79A\": \"vivo Y79\",\n          \"vivo Y83A\": \"vivo Y83（HIH-PHO-3360）\",\n          \"vivo Y85A\": \"vivo Y85\",\n          \"vivo Y913\": \"vivo Y13L\",\n          \"vivo Z1\": \"vivo Z1（HIH-PHO-3368）\",\n          \"vivo Z1i\": \"vivo Z1i（HIH-PHO-3506）\",\n          \"vivo\": \"vivo Y75\",\n          \"VKY-AL00\": \"华为 P10 Plus\",\n          \"VTR-AL00\": \"华为 P10\",\n          \"W800\": \"金立 天鉴 w800\",\n          \"WAS-AL00\": \"华为 nova 青春版\",\n          \"X9007\": \"OPPO Find 7 轻装版\",\n          \"X9077\": \"OPPO Find 7\",\n          \"XT1052\": \"Moto X（一代）\",\n          \"XT1060\": \"Moto X（一代）\",\n          \"XT1079\": \"Moto G 二代\",\n          \"XT1085\": \"Moto X 二代\",\n          \"XT1096\": \"Moto X 二代\",\n          \"XT1570\": \"Moto X Style\",\n          \"XT1635-03\": \"Moto Z Play\",\n          \"XT1650\": \"Moto Z\",\n          \"XT1650-05\": \"Moto Z\",\n          \"XT1662\": \"Moto M（XT1662）\",\n          \"XT1710-08\": \"Moto Z2 Play\",\n          \"XT1789-05\": \"Moto Z 2018\",\n          \"XT1799-2\": \"Moto 青柚\",\n          \"XT907\": \"Moto XT907\",\n          \"Y13L\": \"vivo Y13L\",\n          \"Y23L\": \"vivo Y23L\",\n          \"Y35A\": \"vivo Y35A\",\n          \"YQ601\": \"锤子 坚果手机\",\n          \"Z999\": \"中兴 Axon M\",\n          \"ZTE A0722\": \"中兴 Blade A4\",\n          \"ZTE BA610C\": \"中兴 远航4\",\n          \"ZTE BV0710\": \"中兴 V7 MAX\",\n          \"ZTE BV0720\": \"中兴 Blade A2\",\n          \"ZTE BV0730\": \"中兴 Blade A2 Plus\",\n          \"ZTE C2017\": \"中兴 天机7Max\",\n          \"ZTE C880U\": \"中兴 Blade A1\",\n          \"ZTE U930HD\": \"中兴 U930 HD\",\n          \"ZTE V0900\": \"中兴 Blade V9\",\n          \"ZTE V975\": \"中兴 V975\",\n          \"ZUK Z1\": \"ZUK Z1（Z1221）\",\n          \"ZUK Z2121\": \"ZUK Z2 Pro（Z2121）\",\n          \"ZUK Z2131\": \"ZUK Z2\",\n          \"ZUK Z2151\": \"ZUK Edge\"\n          }\n"
  },
  {
    "path": "uiautomator2/ext/perf/README.md",
    "content": "# Performance 性能采集\n自动记录测试过程中的CPU，PSS, NET\n\n使用方法\n```python\nimport uiautomator2 as u2\nimport uiautomator2.ext.perf as perf\n\npackage_name = \"com.netease.cloudmusic\"\nu2.plugin_register('perf', perf.Perf)\n\n\ndef main():\n    d = u2.connect()\n    d.ext_perf.package_name = package_name\n    d.ext_perf.csv_output = \"perf.csv\" # 保存数据到perf.csv\n    # d.debug = True # 采集到数据就输出，默认关闭\n    # d.interval = 1.0 # 数据采集间隔，默认1.0s，尽量不要小于0.5s，因为采集内存比较费时间\n    d.ext_perf.start()\n\n    # run ... tests code here ...\n    d.ext_perf.stop() # 最好结束的时候调用下，虽然不调用也没多大关系\n    \n    # generate images from csv\n    # 需要安装 matplotlib, pandas, numpy, humanize\n    d.ext_perf.csv2images()\n\n\nif __name__ == '__main__':\n    main()\n```\n\n保存的csv文件内容格式为\n\n```csv\ntime,package,pss,cpu,systemCpu,rxBytes,txBytes,fps\n2018-09-11 20:35:29.016,com.tencent.tmgp.sgame,456.71,13.3,15.8,0,0,12.8\n2018-09-11 20:35:29.733,com.tencent.tmgp.sgame,456.75,11.0,20.6,108,160,30.7\n2018-09-11 20:35:30.756,com.tencent.tmgp.sgame,456.83,12.2,18.9,548,2021,31.3\n2018-09-11 20:35:31.730,com.tencent.tmgp.sgame,457.05,11.6,19.1,160,1199,29.8\n2018-09-11 20:35:32.759,com.tencent.tmgp.sgame,457.05,11.7,19.5,108,160,31.1\n2018-09-11 20:35:33.821,com.tencent.tmgp.sgame,456.86,11.6,17.7,0,0,29.2\n```\n\n生成的图片为\n\n![net](net.png)\n\n![summary](summary.png)\n\n`csv2images`函数更多的用法\n\n```python\nd.ext_perf.csv2images(\"perf.csv\", target_dir=\"./\")\n```\n\n数据项说明\n\n- PSS直接通过`dumpsys meminfo <package-name>`获取\n- CPU直接读取的`/proc/`下的文件计算出来的，多核的情况，数据是有可能超过100%的\n- rxBytes, txBytes 目前只有wlan的流量，tcp和udp的流量总和\n- fps 通过解析`dumpsys SurfaceFlinger --list` 和 `dumpsys SurfaceFlinger --latency <VIEW>` 计算出来\n\n## 参考资料\n- [Python CSV读写方法](https://python3-cookbook.readthedocs.io/zh_CN/latest/c06/p01_read_write_csv_data.html)\n- [android屏幕刷新显示机制](https://blog.csdn.net/litefish/article/details/53939882)\n- [Android FPS计算方法](https://www.jianshu.com/p/1fe9783d266b)\n- [Github项目@leekinwa-androidTestTools_performance_FPS](https://github.com/leekinwa/androidTestTools_Performance_FPS)\n- [官方proc文件格式资料](http://man7.org/linux/man-pages/man5/proc.5.html)\n- [Chromium有关FPS的计算方法](https://github.com/ChromiumWebApps/chromium/blob/master/build/android/pylib/perf/surface_stats_collector.py)\n- [FPS 计算方法的比较 by fenfenzhong](https://testerhome.com/topics/4643)\n- [安卓性能测试之cpu占用率统计方法总结](https://www.jianshu.com/p/6bf564f7cdf0)\n- [Android 性能测试实践 (四) 流量](https://testerhome.com/topics/2643)"
  },
  {
    "path": "uiautomator2/ext/perf/__init__.py",
    "content": "# coding: utf-8\n#\n\nfrom __future__ import absolute_import, print_function\n\nimport atexit\nimport csv\nimport datetime\nimport os\nimport re\nimport sys\nimport threading\nimport time\nfrom collections import namedtuple\n\n_MEM_PATTERN = re.compile(r'TOTAL[:\\s]+(\\d+)')\n# acct_tag_hex is a socket tag\n# cnt_set==0 are for background data\n# cnt_set==1 are for foreground data\n_NetStats = namedtuple(\n    \"NetStats\",\n    \"\"\"idx iface acct_tag_hex uid_tag_int cnt_set rx_bytes rx_packets\n    tx_bytes tx_packets rx_tcp_bytes rx_tcp_packets rx_udp_bytes rx_udp_packets rx_other_bytes rx_other_packets\n    tx_tcp_bytes tx_tcp_packets tx_udp_bytes tx_udp_packets tx_other_bytes tx_other_packets\"\"\"\n    .split())\n\n\nclass Perf(object):\n    def __init__(self, d, package_name=None):\n        self.d = d\n        self.package_name = package_name\n        self.csv_output = \"perf.csv\"\n        self.debug = False\n        self.interval = 1.0\n        self._th = None\n        self._event = threading.Event()\n        self._condition = threading.Condition()\n        self._data = {}\n\n    def shell(self, *args, **kwargs):\n        # print(\"Shell:\", args)\n        return self.d.shell(*args, **kwargs)\n\n    def memory(self):\n        \"\"\" PSS(KB) \"\"\"\n        output = self.shell(['dumpsys', 'meminfo', self.package_name]).output\n        m = _MEM_PATTERN.search(output)\n        if m:\n            return int(m.group(1))\n        return 0\n\n    def _cpu_rawdata_collect(self, pid):\n        \"\"\"\n        pjiff maybe 0 if /proc/<pid>stat not exists\n        \"\"\"\n        first_line = self.shell(['cat', '/proc/stat']).output.splitlines()[0]\n        assert first_line.startswith('cpu ')\n        # ds: user, nice, system, idle, iowait, irq, softirq, stealstolen, guest, guest_nice\n        ds = list(map(int, first_line.split()[1:]))\n        total_cpu = sum(ds)\n        idle = ds[3]\n\n        proc_stat = self.shell(['cat',\n                                '/proc/%d/stat' % pid]).output.split(') ')\n        pjiff = 0\n        if len(proc_stat) > 1:\n            proc_values = proc_stat[1].split()\n            utime = int(proc_values[11])\n            stime = int(proc_values[12])\n            pjiff = utime + stime\n        return (total_cpu, idle, pjiff)\n\n    def cpu(self, pid):\n        \"\"\" CPU\n\n        Refs:\n        - http://man7.org/linux/man-pages/man5/proc.5.html\n        - [安卓性能测试之cpu占用率统计方法总结](https://www.jianshu.com/p/6bf564f7cdf0)\n        \"\"\"\n        store_key = 'cpu-%d' % pid\n        # first time jiffies, t: total, p: process\n        if store_key in self._data:\n            tjiff1, idle1, pjiff1 = self._data[store_key]\n        else:\n            tjiff1, idle1, pjiff1 = self._cpu_rawdata_collect(pid)\n            time.sleep(.3)\n\n        # second time jiffies\n        self._data[\n            store_key] = tjiff2, idle2, pjiff2 = self._cpu_rawdata_collect(pid)\n\n        # calculate\n        pcpu = 0.0\n        if pjiff1 > 0 and pjiff2 > 0:\n            pcpu = 100.0 * (pjiff2 - pjiff1) / (tjiff2 - tjiff1)  # process cpu\n        scpu = 100.0 * ((tjiff2 - idle2) -\n                        (tjiff1 - idle1)) / (tjiff2 - tjiff1)  # system cpu\n        assert scpu > -1  # maybe -0.5, sometimes happens\n        scpu = max(0, scpu)\n        return round(pcpu, 1), round(scpu, 1)\n\n    def netstat(self, pid):\n        \"\"\"\n        Returns:\n            (rall, tall, rtcp, ttcp, rudp, tudp)\n        \"\"\"\n        m = re.search(r'^Uid:\\s+(\\d+)',\n                      self.shell(['cat', '/proc/%d/status' % pid]).output,\n                      re.M)\n        if not m:\n            return (0, 0, 0, 0, 0, 0)\n        uid = m.group(1)\n        lines = self.shell(['cat',\n                            '/proc/net/xt_qtaguid/stats']).output.splitlines()\n\n        traffic = [0] * 6\n\n        def plus_array(arr, *args):\n            for i, v in enumerate(args):\n                arr[i] = arr[i] + int(v)\n\n        for line in lines:\n            vs = line.split()\n            if len(vs) != 21:\n                continue\n            v = _NetStats(*vs)\n            if v.uid_tag_int != uid:\n                continue\n            if v.iface != 'wlan0':\n                continue\n            # all, tcp, udp\n            plus_array(traffic, v.rx_bytes, v.tx_bytes, v.rx_tcp_bytes,\n                       v.tx_tcp_bytes, v.rx_udp_bytes, v.tx_udp_bytes)\n\n        store_key = 'netstat-%s' % uid\n        result = []\n        if store_key in self._data:\n            last_traffic = self._data[store_key]\n            for i in range(len(traffic)):\n                result.append(traffic[i] - last_traffic[i])\n        self._data[store_key] = traffic\n        return result or [0] * 6\n\n    def _current_view(self, app=None):\n        d = self.d\n        views = self.shell(['dumpsys', 'SurfaceFlinger',\n                            '--list']).output.splitlines()\n        if not app:\n            app = d.app_current()\n        current = app['package'] + \"/\" + app['activity']\n        surface_curr = 'SurfaceView - ' + current\n        if surface_curr in views:\n            return surface_curr\n        return current\n\n    def _dump_surfaceflinger(self, view):\n        valid_lines = []\n        MAX_N = 9223372036854775807\n        for line in self.shell(\n            ['dumpsys', 'SurfaceFlinger', '--latency',\n             view]).output.splitlines():\n            fields = line.split()\n            if len(fields) != 3:\n                continue\n            a, b, c = map(int, fields)\n            if a == 0:\n                continue\n            if MAX_N in (a, b, c):\n                continue\n            valid_lines.append((a, b, c))\n        return valid_lines\n\n    def _fps_init(self):\n        view = self._current_view()\n        self.shell([\"dumpsys\", \"SurfaceFlinger\", \"--latency-clear\", view])\n        self._data['fps-start-time'] = time.time()\n        self._data['fps-last-vsync'] = None\n        self._data['fps-inited'] = True\n\n    def fps(self, app=None):\n        \"\"\"\n        Return float\n        \"\"\"\n        if 'fps-inited' not in self._data:\n            self._fps_init()\n            time.sleep(.2)\n        view = self._current_view(app)\n        values = self._dump_surfaceflinger(view)\n        last_vsync = self._data.get('fps-last-vsync')\n        last_start = self._data.get('fps-start-time')\n        try:\n            idx = values.index(last_vsync)\n            values = values[idx + 1:]\n        except ValueError:\n            pass\n        duration = time.time() - last_start\n        if len(values):\n            self._data['fps-last-vsync'] = values[-1]\n        self._data['fps-start-time'] = time.time()\n        return round(len(values) / duration, 1)\n\n    def collect(self):\n        pid = self.d._pidof_app(self.package_name)\n        if pid is None:\n            return\n        app = self.d.app_current()\n        pss = self.memory()\n        cpu, scpu = self.cpu(pid)\n        rbytes, tbytes, rtcp, ttcp = self.netstat(pid)[:4]\n        fps = self.fps(app)\n        timestr = datetime.datetime.now().strftime(\"%Y-%m-%d %H:%M:%S.%f\")[:-3]\n        return {\n            'time': timestr,\n            'package': app['package'],\n            'pss': round(pss / 1024.0, 2),  # MB\n            'cpu': cpu,\n            'systemCpu': scpu,\n            'rxBytes': rbytes,\n            'txBytes': tbytes,\n            'rxTcpBytes': rtcp,\n            'txTcpBytes': ttcp,\n            'fps': fps,\n        }\n\n    def continue_collect(self, f):\n        try:\n            headers = [\n                'time', 'package', 'pss', 'cpu', 'systemCpu', 'rxBytes',\n                'txBytes', 'rxTcpBytes', 'txTcpBytes', 'fps'\n            ]\n            fcsv = csv.writer(f)\n            fcsv.writerow(headers)\n            update_time = time.time()\n            while not self._event.isSet():\n                perfdata = self.collect()\n                if self.debug:\n                    print(\"DEBUG:\", perfdata)\n                if not perfdata:\n                    print(\"perf package is not alive:\", self.package_name)\n                    time.sleep(1)\n                    continue\n                fcsv.writerow([perfdata[k] for k in headers])\n                wait_seconds = max(0,\n                                   self.interval - (time.time() - update_time))\n                time.sleep(wait_seconds)\n                update_time = time.time()\n            f.close()\n        finally:\n            self._condition.acquire()\n            self._th = None\n            self._condition.notify()\n            self._condition.release()\n\n    def start(self):\n        csv_dir = os.path.dirname(self.csv_output)\n        if not os.path.isdir(csv_dir):\n            os.makedirs(csv_dir)\n        if sys.version_info.major < 3:\n            f = open(self.csv_output, \"wb\")\n        else:\n            f = open(self.csv_output, \"w\", newline='\\n')\n\n        def defer_close():\n            if not f.closed:\n                f.close()\n\n        atexit.register(defer_close)\n\n        if self._th:\n            raise RuntimeError(\"perf is already running\")\n        if not self.package_name:\n            raise EnvironmentError(\"package_name need to be set\")\n        self._data.clear()\n        self._event = threading.Event()\n        self._condition = threading.Condition()\n        self._th = threading.Thread(target=self.continue_collect, args=(f, ))\n        self._th.daemon = True\n        self._th.start()\n\n    def stop(self):\n        self._event.set()\n        self._condition.acquire()\n        self._condition.wait(timeout=2)\n        self._condition.release()\n        if self.debug:\n            print(\"DEBUG: perf collect stopped\")\n\n    def csv2images(self, src=None, target_dir='.'):\n        \"\"\"\n        Args:\n            src: csv file, default to perf record csv path\n            target_dir: images store dir\n        \"\"\"\n        import datetime\n        import os\n\n        import matplotlib.pyplot as plt\n        import matplotlib.ticker as ticker\n        import pandas as pd\n\n        from uiautomator2.utils import natualsize\n\n        src = src or self.csv_output\n        if not os.path.exists(target_dir):\n            os.makedirs(target_dir)\n        data = pd.read_csv(src)\n        data['time'] = data['time'].apply(\n            lambda x: datetime.datetime.strptime(x, \"%Y-%m-%d %H:%M:%S.%f\"))\n\n        timestr = time.strftime(\"%Y-%m-%d %H:%M\")\n        # network\n        rx_str = natualsize(data['rxBytes'].sum())\n        tx_str = natualsize(data['txBytes'].sum())\n        plt.subplot(2, 1, 1)\n        plt.plot(data['time'], data['rxBytes'] / 1024, label='all')\n        plt.plot(data['time'], data['rxTcpBytes'] / 1024, 'r--', label='tcp')\n        plt.legend()\n        plt.title(\n            '\\n'.join(\n                [\"Network\", timestr,\n                 'Recv %s, Send %s' % (rx_str, tx_str)]),\n            loc='left')\n        plt.gca().xaxis.set_major_formatter(ticker.NullFormatter())\n        plt.ylabel('Recv(KB)')\n        plt.ylim(ymin=0)\n\n        plt.subplot(2, 1, 2)\n        plt.plot(data['time'], data['txBytes'] / 1024, label='all')\n        plt.plot(data['time'], data['txTcpBytes'] / 1024, 'r--', label='tcp')\n        plt.legend()\n        plt.xlabel('Time')\n        plt.ylabel('Send(KB)')\n        plt.ylim(ymin=0)\n        plt.savefig(os.path.join(target_dir, \"net.png\"))\n        plt.clf()\n\n        plt.subplot(3, 1, 1)\n\n        plt.title(\n            '\\n'.join(['Summary', timestr, self.package_name]), loc='left')\n        plt.plot(data['time'], data['pss'], '-')\n        plt.ylabel('PSS(MB)')\n        plt.gca().xaxis.set_major_formatter(ticker.NullFormatter())\n\n        plt.subplot(3, 1, 2)\n        plt.plot(data['time'], data['cpu'], '-')\n        plt.ylim(0, max(100, data['cpu'].max()))\n        plt.ylabel('CPU')\n        plt.gca().xaxis.set_major_formatter(ticker.NullFormatter())\n\n        plt.subplot(3, 1, 3)\n        plt.plot(data['time'], data['fps'], '-')\n        plt.ylabel('FPS')\n        plt.ylim(0, 60)\n        plt.xlabel('Time')\n        plt.savefig(os.path.join(target_dir, \"summary.png\"))\n\n\nif __name__ == '__main__':\n    import uiautomator2 as u2\n    pkgname = \"com.tencent.tmgp.sgame\"\n    # pkgname = \"com.netease.cloudmusic\"\n    u2.plugin_register('perf', Perf, pkgname)\n\n    d = u2.connect()\n    print(d.app_current())\n    # print(d.ext_perf.netstat(5350))\n    # d.app_start(pkgname)\n    d.ext_perf.start()\n    d.ext_perf.debug = True\n    try:\n        time.sleep(500)\n    except KeyboardInterrupt:\n        d.ext_perf.stop()\n        d.ext_perf.csv2images()\n        print(\"threading stopped\")"
  },
  {
    "path": "uiautomator2/image.py",
    "content": "# coding: utf-8\n#\n# Refs:\n# - https://opencv-python-tutroals.readthedocs.io/en/latest/\n\nimport base64\nimport io\nimport logging\nimport os\nimport re\nimport time\nimport typing\nfrom typing import Union\n\nimport cv2\nimport findit\nimport imutils\nimport numpy as np\nimport requests\nfrom PIL import Image, ImageDraw\nfrom skimage.metrics import structural_similarity\n\nimport uiautomator2\n\nImageType = typing.Union[np.ndarray, Image.Image]\n\ncompare_ssim = structural_similarity\nlogger = logging.getLogger(__name__)\n\n\ndef color_bgr2gray(image: ImageType):\n    \"\"\" change color image to gray\n    Returns:\n        opencv-image\n    \"\"\"\n    if ispil(image):\n        image = pil2cv(image)\n\n    if len(image.shape) == 2:\n        return image\n    return cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)\n\n\ndef template_ssim(image_a: ImageType, image_b: ImageType):\n    \"\"\"\n    Refs:\n        https://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_template_matching/py_template_matching.html\n    \"\"\"\n    a = color_bgr2gray(image_a)\n    b = color_bgr2gray(image_b) # template (small)\n    res = cv2.matchTemplate(a, b, cv2.TM_CCOEFF_NORMED)\n    min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res)\n    return max_val\n\n\ndef cv2crop(im: np.ndarray, bounds: tuple = None):\n    if not bounds:\n        return im\n    assert len(bounds) == 4\n\n    lx, ly, rx, ry = bounds \n    crop_img = im[ly:ry, lx:rx]\n    return crop_img\n\n\ndef compare_ssim(image_a: ImageType, image_b: ImageType, full=False, bounds=None):\n    a = color_bgr2gray(image_a)\n    b = color_bgr2gray(image_b) # template (small)\n    ca = cv2crop(a, bounds)\n    cb = cv2crop(b, bounds)\n    return structural_similarity(ca, cb, full=full)\n\n\ndef compare_ssim_debug(image_a: ImageType, image_b: ImageType, color=(255, 0, 0)):\n    \"\"\"\n    Args:\n        image_a, image_b: opencv image or PIL.Image\n        color: (r, g, b) eg: (255, 0, 0) for red\n\n    Refs:\n        https://www.pyimagesearch.com/2017/06/19/image-difference-with-opencv-and-python/\n    \"\"\"\n    ima, imb = conv2cv(image_a), conv2cv(image_b)\n    score, diff = compare_ssim(ima, imb, full=True)\n    diff = (diff * 255).astype('uint8')\n    _, thresh = cv2.threshold(diff, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)\n    cnts = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)\n    cnts = imutils.grab_contours(cnts)\n\n    cv2color = tuple(reversed(color))\n    im = ima.copy()\n    for c in cnts:\n        x, y, w, h = cv2.boundingRect(c)\n        cv2.rectangle(im, (x, y), (x+w, y+h), cv2color, 2)\n    # todo: show image\n    cv2pil(im).show()\n    return im\n\n\ndef show_image(im: Union[np.ndarray, Image.Image]):\n    pilim = conv2pil(im)\n    pilim.show()\n\n\ndef pil2cv(pil_image) -> np.ndarray:\n    \"\"\" Convert from pillow image to opencv \"\"\"\n    # convert PIL to OpenCV\n    pil_image = pil_image.convert('RGB')\n    cv2_image = np.array(pil_image)\n    # Convert RGB to BGR\n    cv2_image = cv2_image[:, :, ::-1].copy()\n    return cv2_image\n\n\ndef pil2base64(pil_image, format=\"JPEG\") -> str:\n    \"\"\" Convert pillow image to base64 \"\"\"\n    buf = io.BytesIO()\n    pil_image.save(buf, format=format)\n    return base64.b64encode(buf.getvalue()).decode('utf-8')\n\n\ndef cv2pil(cv_image):\n    \"\"\" Convert opencv to pillow image \"\"\"\n    return Image.fromarray(cv_image[:, :, ::-1].copy())\n\n\ndef iscv2(im):\n    return isinstance(im, np.ndarray)\n\n\ndef ispil(im):\n    return isinstance(im, Image.Image)\n\n\ndef conv2cv(im: Union[np.ndarray, Image.Image]) -> np.ndarray:\n    if iscv2(im):\n        return im\n    if ispil(im):\n        return pil2cv(im)\n    raise TypeError(\"Unknown image type:\", type(im))\n\n\ndef conv2pil(im: Union[np.ndarray, Image.Image]) -> Image.Image:\n    if ispil(im):\n        return im\n    elif iscv2(im):\n        return cv2pil(im)\n    else:\n        raise TypeError(f\"Unknown image type: {type(im)}\")\n\n\ndef _open_data_url(data, flag=cv2.IMREAD_COLOR):\n    pos = data.find('base64,')\n    if pos == -1:\n        raise IOError(\"data url is invalid, head %s\" % data[:20])\n\n    pos += len('base64,')\n    raw_data = base64.decodestring(data[pos:])\n    image = np.asarray(bytearray(raw_data), dtype=\"uint8\")\n    image = cv2.imdecode(image, flag)\n    return image\n\n\ndef _open_image_url(url: str, flag=cv2.IMREAD_COLOR):\n    \"\"\" download the image, convert it to a NumPy array, and then read\n    it into OpenCV format \"\"\"\n    content = requests.get(url).content\n    image = np.asarray(bytearray(content), dtype=\"uint8\")\n    image = cv2.imdecode(image, flag)\n    return image\n\n\ndef draw_point(im: Image.Image, x: int, y: int) -> Image.Image:\n    \"\"\"\n    Mark position to show which point clicked\n\n    Args:\n        im: pillow.Image\n    \"\"\"\n    draw = ImageDraw.Draw(im)\n    w, h = im.size\n    draw.line((x, 0, x, h), fill='red', width=5)\n    draw.line((0, y, w, y), fill='red', width=5)\n    r = min(im.size) // 40\n    draw.ellipse((x - r, y - r, x + r, y + r), fill='red')\n    r = min(im.size) // 50\n    draw.ellipse((x - r, y - r, x + r, y + r), fill='white')\n    del draw\n    return im\n\n\ndef imread(data) -> np.ndarray:\n    \"\"\"\n    Args:\n        data: local path or http url or data:image/base64,xxx\n    \n    Returns:\n        opencv image\n    \n    Raises:\n        IOError\n    \"\"\"\n    if isinstance(data, np.ndarray):\n        return data\n    elif isinstance(data, Image.Image):\n        return pil2cv(data)\n    elif data.startswith('data:image/'):\n        return _open_data_url(data)\n    elif re.match(r'^https?://', data):\n        return _open_image_url(data)\n    elif os.path.isfile(data):\n        im = cv2.imread(data)\n        if im is None:\n            raise IOError(\"Image format error: %s\" % data)\n        return im\n\n    raise IOError(\"image read invalid data: %s\" % data)\n\n\nclass ImageX(object):\n    def __init__(self, d: \"uiautomator2.Device\"):\n        \"\"\"\n        Args:\n            d (uiautomator2 instance)\n        \"\"\"\n        self._d = d\n        assert hasattr(d, 'click')\n        assert hasattr(d, 'screenshot')\n\n    def send_click(self, x, y):\n        return self._d.click(x, y)\n    \n    def getpixel(self, x, y):\n        \"\"\"\n        Returns:\n            (r, g, b)\n        \"\"\"\n        screenshot = self._d.screenshot()\n        return screenshot.convert(\"RGB\").getpixel((x, y))\n\n    def match(self, imdata: Union[np.ndarray, str, Image.Image]):\n        \"\"\"\n        Args:\n            imdata: file, url, pillow or opencv image object\n        \n        Returns:\n            templateMatch result\n        \"\"\"\n        cvimage = imread(imdata)\n        fi = findit.FindIt(engine=['template'],\n                           engine_template_scale=(0.9, 1.1, 3),\n                           pro_mode=True)\n        fi.load_template(\"template\", pic_object=cvimage)\n        th, tw = cvimage.shape[:2] # template width, height\n\n        target = self._d.screenshot(format='opencv')\n        assert isinstance(target, np.ndarray), \"screenshot is not opencv format\"\n        raw_result = fi.find(\"target\", target_pic_object=target)\n        # from pprint import pprint\n        # pprint(raw_result)\n        \n        result = raw_result['data']['template']['TemplateEngine']\n        # compress_rate = result['conf']['engine_template_compress_rate'] # useless\n        target_sim = result['target_sim']  # 相似度  similarity\n        x, y = result['target_point'] # this is middle point\n        # x, y = lx+tw//2, ly+th//2\n        return {\"similarity\": target_sim, \"point\": [x, y]}\n\n    def __wait(self, imdata, timeout=30.0, threshold=0.8):\n        deadline = time.time() + timeout\n        while time.time() < deadline:\n            m = self.match(imdata)\n            sim = m['similarity']\n            logger.debug(\"similarity %.2f [~%.2f], left time: %.1fs\", sim,\n                              threshold, deadline - time.time())\n            if sim < threshold:\n                continue\n            time.sleep(.1)\n            return m\n        logger.debug(\"image not found\")\n\n    def wait(self, imdata, timeout=30.0, threshold=0.9):\n        \"\"\" wait until image show up \"\"\"\n        m = self.__wait(imdata, timeout=timeout, threshold=threshold)\n        return m\n\n    def click(self, imdata, timeout=30.0, threshold=0.9):\n        \"\"\"\n        Args:\n            imdata: file, url, pillow or opencv image object\n        \"\"\"\n        res = self.wait(imdata, timeout=timeout, threshold=threshold)\n        if res is None:\n            raise RuntimeError(\"image object not found\")\n        x, y = res['point']\n        return self.send_click(x, y)\n\n\ndef _main():\n    ima = imread(\"http://localhost:17310/widgets/00006/template.jpg\")\n    imb = imread(\"http://localhost:17310/widgets/00007/template.jpg\")\n    compare_ssim_debug(ima, imb, color=(0, 0, 255))\n    return\n    im = imread(\"https://www.baidu.com/img/bd_logo1.png\")\n    assert im.shape == (258, 540, 3)\n    print(im.shape)\n\n    im = imread(\"../tests/testdata/AE86.jpg\")\n    print(im.shape)\n    assert im.shape == (193, 321, 3)\n\n    pim = cv2pil(im)\n    assert pim.size == (321, 193)\n\n    taobao = imread(\"screenshot.jpg\")\n\n    fi = findit.FindIt(engine=['template'],\n                       engine_template_scale=(1, 1, 1),\n                       pro_mode=True)\n    fi.load_template(\"template\", pic_object=taobao)\n\n\nif __name__ == \"__main__\":\n    _main()\n\n    # import uiautomator2 as u2\n    # d = u2.connect()\n    # bg = d.screenshot(format=\"opencv\")\n    # res = fi.find(\"target\", target_pic_object=bg)\n    # from pprint import pprint\n    # pprint(res)\n    # {'target_name': 'target',\n    #  'target_path': None,\n    #  'data': {\n    #       'template': {\n    #           'TemplateEngine': {\n    #               'conf': {\n    #                   'engine_template_cv_method_name': 'cv2.TM_CCOEFF_NORMED',\n    #                   'engine_template_cv_method_code': 5,\n    #                   'engine_template_scale': (1, 1, 1),\n    #                   'engine_template_multi_target_max_threshold': 0.99,\n    #                   'engine_template_multi_target_distance_threshold': 10.0,\n    #                   'engine_template_compress_rate': 1.0},\n    #               'target_point': [111, 1713],\n    #               'target_sim': 0.9984192848205566,\n    #               'raw': {'min_val': -0.4805332124233246,\n    #               'max_val': 0.9984192848205566,\n    #               'min_loc': [990, 1266],\n    #               'max_loc': [111, 1713],\n    #               'all': [[111.0, 1713.5]]},\n    #               'ok': True}}}}\n    # x, y = res[\"data\"][\"template\"][\"TemplateEngine\"][\"target_point\"]\n    # d.click(x, y)\n"
  },
  {
    "path": "uiautomator2/screenrecord.py",
    "content": "# coding: utf-8\n#\n\nimport re\nimport threading\nimport time\n\nimport cv2\nimport imageio\nimport numpy as np\nfrom websocket import create_connection\n\nimport uiautomator2 as u2\n\n\ndef iter_image_from_minicap(uri):\n    ws = create_connection(uri)\n    try:\n        while True:\n            msg = ws.recv()\n            if isinstance(msg, str):\n                print(\"<-\", msg)\n            else:\n                yield msg\n    finally:\n        ws.close()\n\n\nclass Screenrecord:\n    def __init__(self, d: u2.Device):\n        self._d = d\n        self._running = False\n        self._stop_event = threading.Event()\n        self._done_event = threading.Event()\n        self._filename = None\n        self._fps = 20  # initial value\n\n    def __call__(self, *args, **kwargs):\n        self._start(*args, **kwargs)\n        return self\n\n    def _iter_minicap(self):\n        http_url = self._d.path2url(\"/minicap\")\n        ws_url = re.sub(\"^http\", \"ws\", http_url)\n        ws = create_connection(ws_url)\n        try:\n            while not self._stop_event.is_set():\n                msg = ws.recv()\n                if isinstance(msg, str):\n                    # print(\"<-\", msg)\n                    pass\n                else:\n                    yield msg\n        finally:\n            ws.close()\n\n    def _resize_to(self, im, framesize):\n        \"\"\"\n        framesize: tuple of (height, width)\n        \"\"\"\n        vh, vw = framesize\n        h, w = im.shape[:2]\n        frame = np.zeros((vh, vw, 3),\n                         dtype=np.uint8)  # create black background canvas\n        sh = vh / h\n        sw = vw / w\n        if sh < sw:\n            h, w = vh, int(sh * w)\n        else:\n            h, w = int(sw * h), vw\n        left, top = (vw - w) // 2, (vh - h) // 2\n        frame[top:top + h, left:left + w, :] = cv2.resize(im, dsize=(w, h))\n        return frame\n\n    def _pipe_resize(self, image_iter):\n        \"\"\" image to same size \"\"\"\n        firstim = next(image_iter)\n        yield firstim\n        vh, vw = firstim.shape[:2]\n        for im in image_iter:\n            if im.shape != firstim.shape:\n                im = self._resize_to(im, (vh, vw))\n            yield im\n\n    def _pipe_convert(self, raw_iter):\n        # raw data -> imageio\n        for raw in raw_iter:\n            yield imageio.imread(raw)\n\n    def _pipe_limit(self, raw_iter):\n        findex = 0\n        fstart = time.time()\n        for raw in raw_iter:\n            elapsed = time.time() - fstart\n            fcount = int(elapsed * self._fps)\n            for _ in range(fcount - findex):\n                yield raw\n            findex = fcount\n\n    def _run(self):\n        pipelines = [self._pipe_limit, self._pipe_convert, self._pipe_resize]\n        _iter = self._iter_minicap()\n        for p in pipelines:\n            _iter = p(_iter)\n\n        with imageio.get_writer(self._filename, fps=self._fps) as wr:\n            for im in _iter:\n                wr.append_data(im)\n        self._done_event.set()\n\n    def _start(self, filename: str, fps: int = 20):\n        if self._running:\n            raise RuntimeError(\"screenrecord is already started\")\n\n        assert isinstance(fps, int)\n        self._filename = filename\n        self._fps = fps\n\n        self._running = True\n        th = threading.Thread(name=\"image2video\", target=self._run)\n        th.daemon = True\n        th.start()\n\n    def stop(self):\n        \"\"\"\n        stop record and finish write video\n        Returns:\n            bool: whether video is recorded.\n        \"\"\"\n        if not self._running:\n            raise RuntimeError(\"screenrecord is not started\")\n        self._stop_event.set()\n        ret = self._done_event.wait(10.0)\n\n        # reset\n        self._stop_event.clear()\n        self._done_event.clear()\n        self._running = False\n        return ret\n"
  },
  {
    "path": "uiautomator2/settings.py",
    "content": "# coding: utf-8\n#\n\nimport logging\nimport pprint\nfrom typing import Any\n\nlogger = logging.getLogger(__name__)\n\nclass Settings(object):\n    \"\"\" 赋值时会检查类型 \"\"\"\n    def __init__(self, d):\n        self._d = d\n\n        self._defaults = {\n            \"wait_timeout\": 20.0,\n            \"xpath_debug\": False,\n            \"operation_delay\": (0, 0),\n            \"operation_delay_methods\": [\"click\", \"swipe\"],\n            \"fallback_to_blank_screenshot\": False,\n            \"max_depth\": 50,\n        }\n\n        self._deprecated_props = {\n            \"click_after_delay\": \"Use operation_delay instead\",\n            \"click_before_delay\": \"Use operation_delay instead\",\n            \"post_delay\": None,\n            \"uiautomator_runtest_app_background\": None,\n        }\n\n        # 设置变量类型\n        self._prop_types = {\n            \"post_delay\": (float, int),\n            \"xpath_debug\": bool,\n            \"fallback_to_blank_screenshot\": bool,\n        }\n        for k, v in self._defaults.items():\n            if k not in self._prop_types:\n                self._prop_types[k] = (float, int) if type(v) in (float, int) else type(v)\n        \n        self._set_methods = {\n            \"operation_delay\": self.__set_operation_delay, \n        }\n\n        # self._get_methods = {\n        #     \"operation_delay\": self.__get_operation_delay,\n        # }\n    \n    def __set_operation_delay(self, value: tuple):\n        \"\"\" 设置操作的(点击)的前后延时 \"\"\"\n        if isinstance(value, (int, float)):\n            value = (value, value)\n            \n        if isinstance(value, (list, tuple)):\n            assert len(value) == 2, \"operation_delay only accept list with two items\"\n        _pre, post = value\n        assert isinstance(_pre, (int, float)), \"operation_delay can only contains int or float\"\n        assert isinstance(post, (int, float)), \"operation_delay can only contains int or float\"\n\n        self._defaults[\"operation_delay\"] = (_pre, post)\n\n    def get(self, key: str) -> Any:\n        return self._defaults.get(key)\n        \n    def _set(self, key: str, val: Any):\n        # call from methods\n        if key in self._set_methods:\n            return self._set_methods[key](val)\n\n        # Deprecated properties\n        if key in self._deprecated_props:\n            reason = self._deprecated_props[key]\n            if not reason:\n                reason = \"{} is deprecated\".format(key)\n            logger.warning(\"d.settings[{}] deprecated: {}\".format(key, reason))\n            return\n        \n        # Invalid properties\n        if key not in self._prop_types:\n            raise AttributeError(\"invalid attribute\", key)\n\n        # Type check\n        if not isinstance(val, self._prop_types[key]):\n            raise TypeError(\"invalid type, only accept: %r\" % self._prop_types[key])\n\n        self._defaults[key] = val\n\n    def __setitem__(self, key: str, val: Any):\n        self._set(key, val)\n\n    def __getitem__(self, key: str) -> Any:\n        if key not in self._defaults:\n            raise RuntimeError(\"invalid key\", key)\n        return self.get(key)\n    \n    def __repr__(self):\n        return pprint.pformat(self._defaults)\n        # return self._defaults\n\n\n# if __name__ == \"__main__\":\n#     settings = Settings(None)\n#     settings.set(\"pre_delay\", 10)\n#     print(settings['pre_delay'])\n#     settings[\"post_delay\"] = 10\n"
  },
  {
    "path": "uiautomator2/swipe.py",
    "content": "# coding: utf-8\n\nfrom typing import Optional, Tuple, Union\n\nfrom ._proto import Direction\n\n\nclass SwipeExt(object):\n    def __init__(self, d):\n        \"\"\"\n        Args：\n            d (uiautomator2.Device)\n        \"\"\"\n        self._d = d\n\n    def __call__(self,\n                 direction: Union[Direction, str],\n                 scale: float = 0.9,\n                 box: Optional[Tuple[int, int, int, int]] = None,\n                 **kwargs):\n        \"\"\"\n        Args:\n            direction (str): one of \"left\", \"right\", \"up\", \"bottom\" or Direction.LEFT\n            scale (float): percent of swipe, range (0, 1.0]\n            box (tuple): None or [lx, ly, rx, ry]\n            kwargs: used as kwargs in d.swipe\n\n        Raises:\n            ValueError\n        \"\"\"\n        def _swipe(_from, _to):\n            self._d.swipe(_from[0], _from[1], _to[0], _to[1], **kwargs)\n\n        if box:\n            lx, ly, rx, ry = box\n        else:\n            lx, ly = 0, 0\n            rx, ry = self._d.window_size()\n\n        width, height = rx - lx, ry - ly\n\n        h_offset = int(width * (1 - scale)) // 2\n        v_offset = int(height * (1 - scale)) // 2\n\n        center = lx + width//2, ly + height//2\n        left = lx + h_offset, ly + height // 2\n        up = lx + width // 2, ly + v_offset\n        right = rx - h_offset, ly + height // 2\n        bottom = lx + width // 2, ry - v_offset\n\n        if direction == Direction.LEFT:\n            _swipe(right, left)\n        elif direction == Direction.RIGHT:\n            _swipe(left, right)\n        elif direction == Direction.UP:\n            _swipe(center, up) # from center to top\n        elif direction == Direction.DOWN:\n            _swipe(center, bottom) # from center to bottom\n        else:\n            raise ValueError(\"Unknown direction:\", direction)\n"
  },
  {
    "path": "uiautomator2/utils.py",
    "content": "# coding: utf-8\n#\n\nimport contextlib\nimport functools\nimport inspect\nimport pathlib\nimport shlex\nimport sys\nimport threading\nimport typing\nimport warnings\nfrom typing import Iterable, List, Tuple, Union\n\nfrom PIL import Image\n\nfrom uiautomator2._proto import Direction\nfrom uiautomator2.exceptions import SessionBrokenError, UiObjectNotFoundError\n\n\n@contextlib.contextmanager\ndef with_package_resource(filename: str) -> typing.Generator[pathlib.Path, None, None]:\n    \"\"\"\n    Context manager to access a package asset file using importlib.resources.\n    \n    Args:\n        filename (str): The name of the file to locate.\n    \n    Yields:\n        str: The full path to the located file.\n    \"\"\"\n    try:\n        from importlib.resources import as_file, files\n    except ImportError:\n        # For Python < 3.9\n        from importlib_resources import as_file, files\n    anchor = files(\"uiautomator2\") / filename\n    with as_file(anchor) as f:\n        if f.exists():\n            yield f\n            return\n    \n    # check if binary folder contains\n    binary_path = pathlib.Path(sys.argv[0])\n    if (binary_path / filename).exists():\n        yield binary_path / filename\n        return\n    \n    # check if running program directory contains\n    if (pathlib.Path.cwd() / filename).exists():\n        yield pathlib.Path.cwd() / filename\n        return\n    \n    raise FileNotFoundError(f\"Resource {filename} not found in uiautomator2 package.\")\n    \n\ndef check_alive(fn):\n    @functools.wraps(fn)\n    def inner(self, *args, **kwargs):\n        if not self.running():\n            raise SessionBrokenError(self._pkg_name)\n        return fn(self, *args, **kwargs)\n\n    return inner\n\n\n_cached_values = {}\n\n\ndef cache_return(fn):\n    @functools.wraps(fn)\n    def inner(*args, **kwargs):\n        key = (fn, args, frozenset(kwargs.items()))\n        value = _cached_values.get(key)\n        if value is not None:\n            return value\n\n        _cached_values[key] = ret = fn(*args, **kwargs)\n        return ret\n\n    return inner\n\n\ndef hooks_wrap(fn):\n    @functools.wraps(fn)\n    def inner(self, *args, **kwargs):\n        name = fn.__name__.lstrip('_')\n        self.server.hooks_apply(\"before\", name, args, kwargs, None)\n        ret = fn(self, *args, **kwargs)\n        self.server.hooks_apply(\"after\", name, args, kwargs, ret)\n\n    return inner\n\n\n# Will be removed in the future\ndef wrap_wait_exists(fn):\n    @functools.wraps(fn)\n    def inner(self, *args, **kwargs):\n        timeout = kwargs.pop('timeout', self.wait_timeout)\n        if not self.wait(timeout=timeout):\n            raise UiObjectNotFoundError({\n                'code': -32002,\n                'message': self.selector.__str__()\n            })\n        return fn(self, *args, **kwargs)\n\n    return inner\n\n\ndef intersect(rect1, rect2):\n    top = rect1[\"top\"] if rect1[\"top\"] > rect2[\"top\"] else rect2[\"top\"]\n    bottom = rect1[\"bottom\"] if rect1[\"bottom\"] < rect2[\"bottom\"] else rect2[\n        \"bottom\"]\n    left = rect1[\"left\"] if rect1[\"left\"] > rect2[\"left\"] else rect2[\"left\"]\n    right = rect1[\"right\"] if rect1[\"right\"] < rect2[\"right\"] else rect2[\n        \"right\"]\n    return left, top, right, bottom\n\n\nclass Exists(object):\n    \"\"\"Exists object with magic methods.\"\"\"\n    def __init__(self, uiobject):\n        self.uiobject = uiobject\n\n    def __nonzero__(self):\n        \"\"\"Magic method for bool(self) python2 \"\"\"\n        return self.uiobject.jsonrpc.exist(self.uiobject.selector)\n\n    def __bool__(self):\n        \"\"\" Magic method for bool(self) python3 \"\"\"\n        return self.__nonzero__()\n\n    def __call__(self, timeout=0):\n        \"\"\"Magic method for self(args).\n\n        Args:\n            timeout (float): exists in seconds\n        \"\"\"\n        if timeout:\n            return self.uiobject.wait(timeout=timeout)\n        return bool(self)\n\n    def __repr__(self):\n        return str(bool(self))\n\n\ndef list2cmdline(args: Union[str, list, tuple]) -> str:\n    if isinstance(args, str):\n        return args\n    return ' '.join(list(map(shlex.quote, args)))\n\n\ndef inject_call(fn, *args, **kwargs):\n    \"\"\"\n    Call function without known all the arguments\n\n    Args:\n        fn: function\n        args: arguments\n        kwargs: key-values\n    \n    Returns:\n        as the fn returns\n    \"\"\"\n    assert callable(fn), \"first argument must be callable\"\n\n    st = inspect.signature(fn)\n    fn_kwargs = {\n        key: kwargs[key]\n        for key in st.parameters.keys() if key in kwargs\n    }\n    ba = st.bind(*args, **fn_kwargs)\n    ba.apply_defaults()\n    return fn(*ba.args, **ba.kwargs)\n\n\ndef natualsize(size: int) -> str:\n    _KB = 1 << 10\n    _MB = 1 << 20\n    _GB = 1 << 30\n\n    if size >= _GB:\n        return '{:.1f} GB'.format(size / _GB)\n    elif size >= _MB:\n        return '{:.1f} MB'.format(size / _MB)\n    else:\n        return '{:.1f} KB'.format(size / _KB)\n\n\ndef swipe_in_bounds(d: \"uiautomator2.Device\",\n                    bounds: Tuple[int, int, int, int],\n                    direction: Union[Direction, str],\n                    scale: float = 0.6):\n    \"\"\"\n    Args:\n        d: Device object\n        bounds: list of [lx, ly, rx, ry]\n        direction: one of [\"left\", \"right\", \"up\", \"down\"]\n        scale: percent of swipe, range (0, 1.0)\n    \n    Raises:\n        AssertionError, ValueError\n    \"\"\"\n    def _swipe(_from, _to):\n        d.swipe(_from[0], _from[1], _to[0], _to[1])\n\n    assert 0 < scale <= 1.0\n    assert len(bounds) == 4\n\n    lx, ly, rx, ry = bounds\n    width, height = rx - lx, ry - ly\n\n    h_offset = int(width * (1 - scale)) // 2\n    v_offset = int(height * (1 - scale)) // 2\n\n    left = lx + h_offset, ly + height // 2\n    up = lx + width // 2, ly + v_offset\n    right = rx - h_offset, ly + height // 2\n    bottom = lx + width // 2, ry - v_offset\n\n    if direction == Direction.LEFT:\n        _swipe(right, left)\n    elif direction == Direction.RIGHT:\n        _swipe(left, right)\n    elif direction == Direction.UP:\n        _swipe(bottom, up)\n    elif direction == Direction.DOWN:\n        _swipe(up, bottom)\n    else:\n        raise ValueError(\"Unknown direction:\", direction)\n\n\ndef thread_safe_wrapper(fn: typing.Callable):\n    @functools.wraps(fn)\n    def inner(self, *args, **kwargs):\n        if not hasattr(self, \"_lock\"):\n            self._lock = threading.Lock()\n\n        with self._lock:\n            return fn(self, *args, **kwargs)\n    \n    return inner\n\n\n\ndef is_version_compatiable(expect_version: str, actual_version: str) -> bool:\n    \"\"\"\n    Check if the actual version is compatiable with the expect version\n\n    Args:\n        expect_version: expect version, e.g. 1.0.0\n        actual_version: actual version, e.g. 1.0.0\n\n    Returns:\n        bool: True if compatiable, otherwise False\n    \"\"\"\n    def _parse_version(version: str):\n        return tuple(map(int, version.split(\".\")))\n\n    evs = _parse_version(expect_version)\n    avs = _parse_version(actual_version)\n    assert len(evs) == len(avs) == 3, \"version format error\"\n    if evs[0] == avs[0]:\n        if evs[1] < avs[1]:\n            return True\n        if evs[1] == avs[1]:\n            return evs[2] <= avs[2]\n    return False\n\n\ndef deprecated(reason):\n    def decorator(func):\n        @functools.wraps(func)\n        def wrapper(*args, **kwargs):\n            warnings.warn(f\"Function '{func.__name__}' is deprecated: {reason}\", DeprecationWarning, stacklevel=2)\n            return func(*args, **kwargs)\n        return wrapper\n    return decorator\n\n\ndef image_convert(im: Image.Image, format: str):\n    if format == \"pillow\":\n        return im\n    if format == \"opencv\":\n        try:\n            import cv2\n            import numpy as np\n            im = im.convert(\"RGB\")\n            return cv2.cvtColor(np.array(im), cv2.COLOR_RGB2BGR)\n        except ImportError:\n            warnings.warn(\"missing lib: cv2 or numpy\")\n            raise\n    raise ValueError(\"Unsupported format:\", format)\n\n"
  },
  {
    "path": "uiautomator2/version.py",
    "content": "# coding: utf-8\n#\n\n# version managed by poetry\n__version__ = '0.0.0'\n\n\n# see release note for details <https://github.com/openatx/android-uiautomator-server/releases>\n__apk_version__ = '2.4.0'\n\n# old apk version history\n# 2.3.3 make float windows smaller\n# 2.3.2 merge pull requests # require atx-agent>=0.10.0\n# 2.3.1 support minicapagent, rotationagent, minitouchagent\n# 2.2.1 fix click bottom(infinitly display) not working bug\n# 2.2.0 add MinitouchAgent instead of /data/local/tmp/minitouch\n# 2.1.1 add show floatWindow support(pm grant, still have no idea), add TC_TREND analysis\n# 2.0.5 add ToastActivity to show toast or just launch and quit\n# 2.0.4 fix floatingWindow crash on Sumsung Android 9\n# 2.0.3 use android.app.Service instead of android.app.intentService to simpfy logic\n# 2.0.2 fix error: AndroidQ Service must be explicit\n# 2.0.1 fix AndroidQ support\n# 2.0.0 remove runWatchersOnWndowsChange, add setToastListener(bool), add floatWindow\n# 1.1.7 fix dumpHierarchy XML charactor error\n# 1.1.6 fix android P support\n# 1.1.5 waitForExists use UiObject2 method first then fallback to UiObject.waitForExists\n# 1.1.4 add ADB_EDITOR_CODE broadcast support, fix bug （toast捕获导致app闪退)\n# 1.1.3 use thread to make watchers.watched faster, try to fix input method type multi\n# 1.1.2 fix count error when have child && sync watched, to prevent watchers.remove error\n# 1.1.1 support toast capture\n# 1.1.0 update uiautomator-v18:2.1.2 -> uiautomator-v18:2.1.3 (This version fixed setWaitIdleTimeout not working bug)\n# 1.0.14 catch NullException, add gps mock support\n# 1.0.13 whatsinput suppoort, but not very well\n# 1.0.12 add toast support\n# 1.0.11 add auto install support\n# 1.0.10 fix service not started bug\n# 1.0.9 fix apk version code and version name\n# ERR: 1.0.8 bad version number. show ip on notification\n# ERR: 1.0.7 bad version number. new input method, some bug fix\n\n# __jar_version__ = 'v0.1.6'  # no useless for now.\n# v0.1.6 first release version\n\n# __atx_agent_version__ = '0.10.1' # sync.sh verison should also be updated\n# 0.10.1 update androidbinary version, https://github.com/openatx/atx-agent/issues/115\n# 0.10.0 remove tunnel code, use androidx.test.runner\n# 0.9.6 fix security reason for remote control device\n# 0.9.5 log support rotate, prevent log too large\n# 0.9.4 test travis push to qiniu-cdn\n# 0.9.3 fix atx-agent version output too many output\n# 0.9.2 fix when /sdcard/atx-agent.log can't create, atx-agent can't start error\n# 0.9.1 update /minicap to use apkagent and minicap\n# 0.9.0 add /minicap/broadcast api, add service(\"apkagent\")\n# 0.8.4 use minicap when sdk less than Android Q\n# 0.8.3 use minitouchagent instead of /data/local/tmp/minitouch\n# 0.8.2 change am instrument maxRetry from 3 to 1\n# 0.8.1 fix --stop can not stop atx-agent error, fix --help format error\n# 0.8.0 add /newCommandTimeout api, ref: appium-newCommandTimeout\n# 0.7.4 add /finfo/{filepath:.*} api\n# 0.7.3 add uiautomator-1.0 support\n# 0.7.2 fix stop already stopped uiautomator return status 500 error\n# 0.7.1 fix UIAutomation not connected error.\n# 0.7.0 add webview support, kill uiautomator if no activity in 3 minutes\n# 0.6.2 fix app_info fd leak error, update androidbinary to fix parse apk manifest err\n# 0.6.1 make dump_hierarchy more robust, add cpu,mem collect\n# 0.6.0 add /dump/hierarchy (works fine even if uiautomator is down)\n# 0.5.5 add minitouch reset, /screenshot support download param, fix dns error\n# 0.5.4 upgrade atx-agent to fix apk parse mainActivity of com.tmall.wireless\n# 0.5.3 try to fix panic in heartbeat\n# 0.5.2 fix /session/${pkgname} launch timeout too short error(before was 10s)\n# 0.5.1 bad tag, deprecated\n# 0.5.0 add /packages/${pkgname}/<info|icon> api\n# 0.4.9 update for go1.11\n# 0.4.8 add /wlan/ip and /packages REST API for package install\n# 0.4.6 fix download dns resolve error (sometimes)\n# 0.4.5 add http log, change atx-agent -d into atx-agent server -d\n# 0.4.4 this version is gone\n# 0.4.3 ignore sigint to prevent atx-agent quit\n# 0.4.2 hot fix, close upgrade-self\n# 0.4.1 fix app-download time.Timer panic error, use safe-time.Timer instead.\n# 0.4.0 add go-daemon lib. use safe-time.Timer to prevent panic error. this will make it run longer\n# 0.3.6 support upload zip and unzip, fix minicap rotation error when atx-agent is killed -9\n# 0.3.5 hot fix for session\n# 0.3.4 fix session() sometimes can not get mainActivity error\n# 0.3.3 /shell support timeout\n# 0.3.2 fix dns resolve error when network changes\n# 0.3.0 use github.com/codeskyblue/heartbeat library instead of websocket, add /whatsinput\n# 0.2.1 support occupy /minicap connection\n# 0.2.0 add session support\n# 0.1.8 fix screenshot always the same image. (BUG in 0.1.7), add /shell/stream add timeout for /shell\n# 0.1.7 fix dns resolve error in /install\n# 0.1.6 change download logic. auto fix orientation\n# 0.1.5 add singlefight for minicap and minitouch, proxy dial-timeout change 30 to 10\n# 0.1.4 phone remote control\n# 0.1.2 /download support\n# 0.1.1 minicap buildin\n"
  },
  {
    "path": "uiautomator2/watcher.py",
    "content": "# coding: utf-8\n#\n\nimport inspect\nimport logging\nimport threading\nimport time\nimport typing\nfrom collections import OrderedDict\nfrom typing import List, Optional\n\nimport uiautomator2\nfrom uiautomator2.utils import inject_call\nfrom uiautomator2.xpath import PageSource, XPathEntry, XPathSelector\n\nlogger = logging.getLogger(__name__)\n\n\ndef _callback_click(el):\n    el.click()\n\n\nclass WatchContext:\n    def __init__(self, d: \"uiautomator2.Device\", builtin: bool = False):\n        self._d = d\n        self._callbacks = OrderedDict()\n        self.__xpath_list = []\n        self.__lock = threading.Lock()\n        self.__trigger_time = time.time()\n\n        # 这里竟然要3个变量记录状态\n        self.__stop = threading.Event()\n        self.__stopped = threading.Event()  # 结束时设置\n        self.__started = False\n\n        if builtin:\n            self.when(\"继续使用\").click()\n            self.when(\"移入管控\").when(\"取消\").click()\n            self.when(\"^立即(下载|更新)\").when(\"取消\").click()\n            self.when(\"同意\").click()\n            self.when(\"^(好的|确定)\").click()\n            self.when(\"继续安装\").click()\n            self.when(\"安装\").click()\n            self.when(\"Agree\").click()\n            self.when(\"ALLOW\").click()\n\n    def wait_stable(self, seconds: float = 5.0, timeout: float = 60.0):\n        \"\"\" wait until watches not triggered\n        Args:\n            seconds: stable seconds\n            timeout: raise error when wait stable timeout\n\n        Raises:\n            TimeoutError\n        \"\"\"\n        if not self.__started:\n            self.start()\n\n        deadline = time.time() + timeout\n        while time.time() < deadline:\n            with self.__lock:\n                if time.time() - self.__trigger_time > seconds:\n                    return True\n            time.sleep(.2)\n        raise TimeoutError(\"Unstable\")\n\n    def when(self, xpath: str):\n        \"\"\" 当条件满足时,支持 .when(..).when(..) 的级联模式\"\"\"\n        self.__xpath_list.append(xpath)\n        return self\n\n    def call(self, fn: typing.Callable):\n        \"\"\"\n        Args:\n            fn: support args (d: Device, el: Element)\n                see _run_callback function for more details\n        \"\"\"\n        xpath_list = tuple(self.__xpath_list)\n        self.__xpath_list = []\n        assert xpath_list, \"when should be called before\"\n\n        self._callbacks[xpath_list] = fn\n\n    def click(self):\n        self.call(_callback_click)\n\n    def _run(self) -> bool:\n        logger.debug(\"watch check\")\n        source = self._d.dump_hierarchy()\n        for xpaths, func in self._callbacks.items():\n            ok = True\n            last_match = None\n            for xpath in xpaths:\n                sel: XPathSelector = self._d.xpath(xpath, source=source)\n                if not sel.exists:\n                    ok = False\n                    break\n                last_match = sel.get_last_match()\n                logger.debug(\"match: %s\", xpath)\n            if ok:\n                # 全部匹配\n                logger.debug(\"watchContext xpath matched: %s\", xpaths)\n                self._run_callback(func, last_match)\n                return True\n        return False\n\n    def _run_callback(self, func, element):\n        inject_call(func, d=self._d, el=element)\n        self.__trigger_time = time.time()\n\n    def _run_forever(self, interval: float):\n        try:\n            while not self.__stop.is_set():\n                with self.__lock:\n                    self._run()\n                time.sleep(interval)\n        finally:\n            self.__stopped.set()\n\n    def start(self):\n        if self.__started:\n            return\n        self.__started = True\n        self.__stop.clear()\n        self.__stopped.clear()\n        interval = 2.0  # 检查周期\n        threading.Thread(target=self._run_forever,\n                         daemon=True,\n                         args=(interval, )).start()\n\n    def stop(self):\n        self.__stop.set()\n        self.__stopped.wait(timeout=10)\n        self.__started = False\n    \n    def close(self):\n        \"\"\" alias of stop \"\"\"\n        self.stop()\n\n    def __enter__(self):\n        return self\n\n    def __exit__(self, type, value, traceback):\n        logger.info(\"context closed\")\n        self.stop()\n\n\nclass Watcher():\n    def __init__(self, d: \"uiautomator2.Device\"):\n        self._d = d\n        self._watchers = []\n\n        self._watch_stop_event = threading.Event()\n        self._watch_stopped = threading.Event()\n        self._watching = False  # func start is calling\n        self._triggering = False\n\n    @property\n    def _xpath(self) -> XPathEntry:\n        return self._d.xpath\n\n    def _dump_hierarchy(self):\n        return self._d.dump_hierarchy()\n\n    def when(self, xpath=None):\n        return XPathWatcher(self, xpath)\n\n    def start(self, interval: float = 2.0):\n        \"\"\" stop watcher \"\"\"\n        if self._watching:\n            logger.warning(\"already started\")\n            return\n        self._watching = True\n        th = threading.Thread(name=\"watcher\",\n                              target=self._watch_forever,\n                              args=(interval, ))\n        th.daemon = True\n        th.start()\n        return th\n\n    def stop(self):\n        \"\"\" stop watcher \"\"\"\n        if not self._watching:\n            logger.warning(\"watch already stopped\")\n            return\n\n        if self._watch_stopped.is_set():\n            return\n\n        self._watch_stopped.set()\n        self._watch_stop_event.wait(timeout=10)\n\n        # reset all status\n        self._watching = False\n        self._watch_stopped.clear()\n        self._watch_stop_event.clear()\n\n    def reset(self):\n        \"\"\" stop watching and remove all watchers \"\"\"\n        if self._watching:\n            self.stop()\n        self.remove()\n\n    def running(self) -> bool:\n        return self._watching\n\n    @property\n    def triggering(self) -> bool:\n        return self._triggering\n\n    def _watch_forever(self, interval: float):\n        try:\n            wait_timeout = interval\n            while not self._watch_stopped.wait(timeout=wait_timeout):\n                triggered = self.run()\n                wait_timeout = min(0.5, interval) if triggered else interval\n        finally:\n            self._watch_stop_event.set()\n\n    def run(self, source: Optional[PageSource] = None):\n        \"\"\" run watchers\n        Args:\n            source: hierarchy content\n        \"\"\"\n        if self.triggering:  # avoid to run watcher when run watcher\n            return False\n        try:\n            return self._run_watchers(source=source)\n        except Exception as e:\n            logger.warning(\"_run_watchers exception: %s\", e)\n            return False\n\n    def _run_watchers(self, source=None) -> bool:\n        \"\"\"\n        Returns:\n            bool (watched or not)\n        \"\"\"\n        source = source or self._xpath.get_page_source()\n\n        for h in self._watchers:\n            last_selector = None\n            for xpath in h['xpaths']:\n                last_selector = self._xpath(xpath, source)\n                if not last_selector.exists:\n                    last_selector = None\n                    break\n\n            if last_selector:\n                logger.info(\"XPath(hook:%s): %s\", h['name'], h['xpaths'])\n                self._triggering = True\n                cb = h['callback']\n                defaults = {\n                    \"selector\": last_selector,\n                    \"d\": self._d,\n                    \"source\": source,\n                }\n                st = inspect.signature(cb)\n                kwargs = {\n                    key: defaults[key]\n                    for key in st.parameters.keys() if key in defaults\n                }\n                ba = st.bind(**kwargs)\n                ba.apply_defaults()\n                try:\n                    cb(*ba.args, **ba.kwargs)\n                except Exception as e:\n                    logger.warning(\"watchers exception: %s\", e)\n                finally:\n                    self._triggering = False\n                return True\n        return False\n\n    def __call__(self, name: str) -> \"XPathWatcher\":\n        return XPathWatcher(self, None, name)\n\n    def remove(self, name=None):\n        \"\"\" remove watcher \"\"\"\n        if name is None:\n            self._watchers = []\n            return\n        for w in self._watchers[:]:\n            if w['name'] == name:\n                logger.debug(\"remove(%s) %s\", name, w['xpaths'])\n                self._watchers.remove(w)\n\n\nclass XPathWatcher():\n    def __init__(self, parent: Watcher, xpath: str, name: str = ''):\n        self._name = name\n        self._parent = parent\n        self._xpath_list: List[str] = [xpath] if xpath else []\n\n    def when(self, xpath: str = None):\n        self._xpath_list.append(xpath)\n        return self\n\n    def call(self, func: callable):\n        \"\"\"\n        func accept argument, key(d, el)\n        d=self._d, el=element\n        \"\"\"\n        self._parent._watchers.append({\n            \"name\": self._name,\n            \"xpaths\": self._xpath_list,\n            \"callback\": func,\n        })\n\n    def click(self):\n        def _inner_click(selector: XPathSelector):\n            selector.get_last_match().click()\n\n        self.call(_inner_click)\n\n    def press(self, key):\n        \"\"\"\n        key (str): on of\n            (\"home\", \"back\", \"left\", \"right\", \"up\", \"down\", \"center\",\n            \"search\", \"enter\", \"delete\", \"del\", \"recent\", \"volume_up\",\n            \"menu\", \"volume_down\", \"volume_mute\", \"camera\", \"power\")\n        \"\"\"\n        def _inner_press(d: \"uiautomator2.Device\"):\n            d.press(key)\n\n        self.call(_inner_press)\n"
  },
  {
    "path": "uiautomator2/xpath.py",
    "content": "# coding: utf-8\n#\n\nfrom __future__ import absolute_import\n\nimport abc\nimport copy\nimport enum\nimport functools\nimport logging\nimport re\nimport time\nfrom typing import Any, Callable, Dict, List, Optional, Tuple, Union\n\nfrom lxml import etree\nfrom PIL import Image\n\nfrom uiautomator2._proto import Direction\nfrom uiautomator2.abstract import AbstractXPathBasedDevice\nfrom uiautomator2.exceptions import XPathElementNotFoundError\nfrom uiautomator2.utils import deprecated, inject_call, swipe_in_bounds\n\nlogger = logging.getLogger(__name__)\n\n\nclass TimeoutException(Exception):\n    pass\n\n\nclass XPathError(Exception):\n    \"\"\"basic error for xpath plugin\"\"\"\n\n\n\ndef safe_xmlstr(s: str) -> str:\n    # https://www.w3.org/TR/xml/#NT-NameStartChar\n    s = s.strip()\n    s = re.sub('[$@#&]', '.', s)\n    s = re.sub('\\\\.+', '.', s)\n    s = re.sub('^\\\\.|\\\\.$', '', s)\n    return s\n\n\ndef string_quote(s: str) -> str:\n    \"\"\"quick way to quote string\"\"\"\n    return \"{!r}\".format(s)\n\n\ndef str2bytes(v: Union[str, bytes]) -> bytes:\n    if isinstance(v, bytes):\n        return v\n    if isinstance(v, str):\n        return v.encode(\"utf-8\")\n    raise ValueError(\"Invalid type\", type(v), v)\n\n\ndef is_xpath_syntax_ok(xpath_expression: str) -> bool:\n    try:\n        etree.XPath(xpath_expression)\n        return True  # No error means the XPath syntax is likely okay\n    except etree.XPathSyntaxError:\n        return False  # Indicates a syntax error in the XPath expression\n\n\ndef convert_to_camel_case(s: str) -> str:\n    \"\"\"\n    Convert a string from kebab-case to camelCase.\n\n    Example:\n        \"hello-world\" -> \"helloWorld\"\n    \"\"\"\n    parts = s.split('-')\n    # Convert the first letter of each part to uppercase, except for the first part\n    camel_case_str = parts[0] + ''.join(part.capitalize() for part in parts[1:])\n    return camel_case_str\n\n\ndef strict_xpath(xpath: str) -> str:\n    \"\"\"make xpath to be computer recognized xpath\"\"\"\n    orig_xpath = xpath\n\n    if xpath.lstrip(\"(\").startswith(\"/\"):\n        pass\n    elif xpath.startswith(\"@\"):\n        xpath = \"//*[@resource-id={!r}]\".format(xpath[1:])\n    elif xpath.startswith(\"^\"):\n        xpath = \"//*[re:match(@text, {0}) or re:match(@content-desc, {0}) or re:match(@resource-id, {0})]\".format(\n            string_quote(xpath)\n        )\n    elif xpath.startswith(\"%\") and xpath.endswith(\"%\"):\n        xpath = \"//*[contains(@text, {0}) or contains(@content-desc, {0})]\".format(\n            string_quote(xpath[1:-1])\n        )\n    elif xpath.startswith(\"%\"):  # ends-with\n        text = xpath[1:]\n        xpath = \"//*[{0} = substring(@text, string-length(@text) - {1} + 1) or {0} = substring(@content-desc, string-length(@text) - {1} + 1)]\".format(\n            string_quote(text), len(text)\n        )\n    elif xpath.endswith(\"%\"):  # starts-with\n        text = xpath[:-1]\n        xpath = (\n            \"//*[starts-with(@text, {0}) or starts-with(@content-desc, {0})]\".format(\n                string_quote(text)\n            )\n        )\n    else:\n        xpath = \"//*[@text={0} or @content-desc={0} or @resource-id={0}]\".format(\n            string_quote(xpath)\n        )\n\n    xpath = xpath.rstrip(\"/\")\n    if not is_xpath_syntax_ok(xpath):\n        raise XPathError(\"Invalid xpath\", orig_xpath)\n    logger.debug(\"xpath %s -> %s\", orig_xpath, xpath)\n    return xpath\n\n\nclass XPath(str):\n    def __new__(cls, value, *args):\n        if isinstance(value, XPath):\n            return value\n        xpath = strict_xpath(value)\n        if args:\n            return functools.reduce(lambda a, b: a.joinpath(b), args, XPath(xpath))\n        else:\n            return super().__new__(cls, xpath)\n    \n    def __repr__(self):\n        return f'XPath({super().__repr__()})'\n\n    def __and__(self, value: 'XPath') -> 'XPathSelector':\n        raise NotImplementedError\n\n    def joinpath(self, subpath: str) -> \"XPath\":\n        if not subpath.startswith('/'):\n            subpath = '/' + subpath\n        return XPath(self + subpath)\n\n    def all(self, source: \"PageSource\"):\n        return source.find_elements(self)\n    \n\nclass PageSource:\n    def __init__(self, xml_content: str):\n        # Remove Left-to-Right Mark, BLM, Zero-Width Space, BOM etc Invisible chars\n        clean_xml_content = re.sub(r'[\\u200B-\\u200F\\uFEFF]', '', xml_content)\n        self._xml_content = clean_xml_content\n    \n    @staticmethod\n    def parse(data: Union[str, \"PageSource\"]) -> \"PageSource\":\n        if isinstance(data, str):\n            return PageSource(data)\n        return data\n    \n    @functools.cached_property\n    def root(self) -> etree._Element:\n        _root = etree.fromstring(str2bytes(self._xml_content))\n        for node in _root.xpath(\"//node\"):\n            node.tag = safe_xmlstr(node.attrib.pop(\"class\", \"\")) or \"node\"\n        return _root\n\n    def find_elements(self, xpath: str) -> List[\"XMLElement\"]:\n        matches = self.root.xpath(xpath, namespaces={\"re\": \"http://exslt.org/regular-expressions\"})\n        return [XMLElement(node) for node in matches]\n\n\nclass XPathEntry(object):\n    def __init__(self, d: AbstractXPathBasedDevice):\n        \"\"\"\n        Args:\n            d (uiautomator2 instance)\n        \"\"\"\n        self._d = d\n        assert hasattr(d, \"wait_timeout\")\n        # TODO: remove wait_timeout\n\n    def global_set(self, key, value):\n        valid_keys = {\n            \"timeout\",\n        }\n        if key not in valid_keys:\n            raise ValueError(\"invalid key\", key)\n        if key == \"timeout\":\n            self.implicitly_wait(value)\n        else:\n            setattr(self, \"_\" + key, value)\n\n    def implicitly_wait(self, timeout):\n        \"\"\"set default timeout when click\"\"\"\n        self._d.wait_timeout = timeout\n\n    @property\n    def wait_timeout(self):\n        return self._d.wait_timeout\n\n    @property\n    def _watcher(self):\n        return self._d.watcher\n\n    def get_page_source(self) -> PageSource:\n        return PageSource.parse(self._d.dump_hierarchy())\n\n    def match(self, xpath, source=None):\n        return len(self(xpath, source).all()) > 0\n\n    @deprecated(reason=\"use d.watcher.when(..) instead\")\n    def when(self, xquery: str):\n        return self._watcher.when(xquery)\n\n    @deprecated(reason=\"use d.watcher.run() instead\")\n    def run_watchers(self, source=None):\n        self._watcher.run()\n\n    @deprecated(reason=\"use d.watcher.start(..) instead\")\n    def watch_background(self, interval: float = 4.0):\n        return self._watcher.start(interval)\n\n    @deprecated(reason=\"use d.watcher.stop() instead\")\n    def watch_stop(self):\n        \"\"\"stop watch background\"\"\"\n        self._watcher.stop()\n\n    @deprecated(reason=\"use d.watcher.remove() instead\")\n    def watch_clear(self):\n        self._watcher.stop()\n\n    @deprecated(reason=\"removed\")\n    def sleep_watch(self, seconds):\n        \"\"\"run watchers when sleep\"\"\"\n        deadline = time.time() + seconds\n        while time.time() < deadline:\n            self.run_watchers()\n            left_time = max(0, deadline - time.time())\n            time.sleep(min(0.5, left_time))\n\n    def click(self, xpath: str, timeout: Optional[float]=None):\n        \"\"\"\n        Find element and perform click\n\n        Args:\n            xpath (str): xpath string\n            timeout (float): pass\n            pre_delay (float): pre delay wait time before click\n\n        Raises:\n            TimeoutException\n        \"\"\"\n        selector = DeviceXPathSelector(xpath, self)\n        selector.click(timeout=timeout)\n\n    def scroll_to(\n        self,\n        xpath: str,\n        direction: Union[Direction, str] = Direction.FORWARD,\n        max_swipes=10,\n    ) -> Union[\"XMLElement\", None]:\n        \"\"\"\n        Need more tests\n        scroll up the whole screen until target element founded\n\n        Returns:\n            bool (found or not)\n        \"\"\"\n        if direction == Direction.FORWARD:\n            direction = Direction.UP\n        elif direction == Direction.BACKWARD:\n            direction = Direction.DOWN\n        elif direction == Direction.HORIZ_FORWARD:  # Horizontal\n            direction = Direction.LEFT\n        elif direction == Direction.HORIZ_BACKWARD:\n            direction = Direction.RIGHT\n        else:\n            raise ValueError(\"Invalid direction\", direction)\n\n        # FIXME(ssx): 还差一个检测是否到底的功能\n        assert max_swipes > 0\n        target = self(xpath)\n        for i in range(max_swipes):\n            if target.exists:\n                self._d.swipe_ext(direction, 0.1)  # 防止元素停留在边缘\n                return target.get_last_match()\n            self._d.swipe_ext(direction, 0.5)\n        return None\n\n    def __call__(self, xpath: str, source: Optional[Union[str, PageSource]] = None) -> \"DeviceXPathSelector\":\n        return DeviceXPathSelector(xpath, self, PageSource.parse(source) if source else None)\n\n\nclass Operator(str, enum.Enum):\n    AND = 'AND'\n    OR = 'OR'\n\n\nclass AbstractSelector(abc.ABC):\n    @abc.abstractmethod\n    def all(self, source: PageSource) -> List['XMLElement']:\n        pass\n    \n\nclass XPathSelector(AbstractSelector):\n    def __init__(self, value: Union[str, XPath, AbstractSelector]):\n        if isinstance(value, str):\n            self._base_xpath = XPath(value)\n        elif isinstance(value, (XPath, AbstractSelector)):\n            self._base_xpath = value\n        else:\n            raise ValueError(\"Invalid type\", type(value), value)\n        self._operator: Optional[Operator] = None\n        self._next_xpath: Optional[AbstractSelector] = None\n\n    def copy(self):\n        \"\"\"copy self\"\"\"\n        return copy.copy(self)\n\n    @classmethod\n    def create(cls, value: Union[str, 'XPathSelector']) -> 'XPathSelector':\n        if isinstance(value, XPathSelector):\n            return value.copy()\n        elif isinstance(value, str):\n            return cls(XPath(value))\n        else:\n            raise ValueError('Invalid value', value)\n    \n    def __repr__(self):\n        if self._operator:\n            return f'#({repr(self._base_xpath)} {self._operator.value} {repr(self._next_xpath)})'\n        else:\n            return f'#({repr(self._base_xpath)})'\n\n    def __and__(self, value) -> 'XPathSelector':\n        s = XPathSelector(self)\n        s._next_xpath = XPathSelector.create(value)\n        s._operator = Operator.AND\n        return s\n\n    def __or__(self, value) -> 'XPathSelector':\n        s = XPathSelector(self)\n        s._next_xpath = XPathSelector.create(value)\n        s._operator = Operator.OR\n        return s\n\n    @deprecated(reason=\"use and_ or & instead\")\n    def xpath(self, _xpath: Union[list, tuple, str]) -> 'XPathSelector':\n        \"\"\"\n        add xpath to condition list\n        the element should match all conditions\n        \"\"\"\n        if isinstance(_xpath, (list, tuple)):\n            return functools.reduce(lambda a, b: a & b, _xpath, self)\n        else:\n            return self & _xpath\n    \n    def child(self, _xpath: str) -> \"XPathSelector\":\n        \"\"\"\n        add child xpath\n        \"\"\"\n        if self._operator or not isinstance(self._base_xpath, XPath):\n            raise XPathError(\"can't use child when base is not XPath or operator is set\")\n        new = self.copy()\n        new._base_xpath = self._base_xpath.joinpath(_xpath)\n        return new\n    \n    def all(self, source: PageSource) -> List[\"XMLElement\"]:\n        \"\"\"find all matched elements\"\"\"\n        elements = self._base_xpath.all(source)\n\n        # AND OR\n        if self._next_xpath and self._operator:\n            next_els = self._next_xpath.all(source)\n            if self._operator == Operator.AND:\n                elements = list(set(elements) & set(next_els))\n            elif self._operator == Operator.OR:\n                elements = list(set(elements) | set(next_els))\n            else:\n                raise ValueError(\"Invalid operator\", self._operator)\n        return elements\n\nclass DeviceXPathSelector(XPathSelector):\n    def __init__(self, xpath: Union[str, AbstractSelector], parent: XPathEntry, source: Optional[PageSource] = None):\n        super().__init__(xpath)\n        self._parent = parent\n        self._source = source\n        self._last_source: Optional[PageSource] = None\n        self._fallback: Optional[Callable] = None\n    \n    def from_parent(self, p: XPathSelector):\n        dp = DeviceXPathSelector(p._base_xpath, self._parent, self._source)\n        dp._operator = p._operator\n        dp._next_xpath = p._next_xpath\n        return dp\n\n    def __and__(self, value) -> 'DeviceXPathSelector':\n        s = super().__and__(value)\n        return self.from_parent(s)\n\n    def __or__(self, value) -> 'DeviceXPathSelector':\n        s = super().__or__(value)\n        return self.from_parent(s)\n\n    def fallback(self, func: Optional[Callable[..., bool]] = None, *args, **kwargs):\n        \"\"\"\n        callback on failure\n        \"\"\"\n        if not callable(func):\n            raise ValueError('func should be \"click\" or callable function')\n    \n        assert callable(func)\n        new = self.copy()\n        new._fallback = func\n        return new\n\n    @property\n    def _global_timeout(self) -> float:\n        if hasattr(self._parent, \"wait_timeout\") and isinstance(self._parent.wait_timeout, (int, float)):\n            return self._parent.wait_timeout\n        return 20.0\n\n    def _get_page_source(self) -> PageSource:\n        if self._source:\n            return self._source\n        if not self._parent:\n            raise XPathError(\"self._parent is not set\")\n        return self._parent.get_page_source()\n    \n\n    def all(self, source: Optional[PageSource] = None) -> List[\"DeviceXMLElement\"]:\n        \"\"\"find all matched elements\"\"\"\n        if not source:\n            source = self._get_page_source()\n        self._last_source = source\n        elements = super().all(source)\n        return [DeviceXMLElement(el, self._parent) for el in elements]\n\n    @property\n    def exists(self) -> bool:\n        return len(self.all()) > 0\n\n    def get(self, timeout=None):\n        \"\"\"\n        Get first matched element\n\n        Args:\n            timeout (float): max seconds to wait\n\n        Returns:\n            XMLElement\n\n        Raises:\n            XPathElementNotFoundError\n        \"\"\"\n        if not self.wait(timeout or self._global_timeout):\n            raise XPathElementNotFoundError(self)\n        return self.get_last_match()\n\n    def get_last_match(self) -> \"DeviceXMLElement\":\n        return self.all(self._last_source)[0]\n\n    def get_text(self) -> Optional[str]:\n        \"\"\"\n        get element text\n\n        Returns:\n            string of node text\n\n        Raises:\n            XPathElementNotFoundError\n        \"\"\"\n        return self.get().text\n\n    def set_text(self, text: str):\n        el = self.get()\n        el.click()  # focus input-area\n        self._parent._d.clear_text()  # type: ignore\n        self._parent._d.send_keys(text)\n\n    def wait(self, timeout=None) -> bool:\n        \"\"\" wait until element found \"\"\"\n        deadline = time.time() + (timeout or self._global_timeout)\n        while True:\n            if self.exists:\n                return True\n            if time.time() > deadline:\n                return False\n            time.sleep(0.2)\n\n    def match(self) -> Optional[\"DeviceXMLElement\"]:\n        \"\"\"\n        Returns:\n            None or matched DeviceXMLElement\n        \"\"\"\n        if self.exists:\n            return self.get_last_match()\n\n    def wait_gone(self, timeout=None) -> bool:\n        \"\"\"\n        Args:\n            timeout (float): seconds\n\n        Returns:\n            True if gone else False\n        \"\"\"\n        deadline = time.time() + (timeout or self._global_timeout)\n        while time.time() < deadline:\n            if not self.exists:\n                return True\n            time.sleep(0.2)\n        return False\n\n    def click_nowait(self):\n        x, y = self.all()[0].center()\n        logger.info(\"click %d, %d\", x, y)\n        self._parent._d.click(x, y)\n\n    def click(self, timeout=None):\n        \"\"\"find element and perform click\"\"\"\n        try:\n            el = self.get(timeout=timeout)\n            el.click()\n        except XPathElementNotFoundError:\n            if not self._fallback:\n                raise\n            logger.info(\"element not found, run fallback\")\n            return inject_call(self._fallback, d=self._d)\n\n    def click_exists(self, timeout=None) -> bool:\n        \"\"\"return if clicked\"\"\"\n        try:\n            el = self.get(timeout=timeout)\n            el.click()\n            return True\n        except XPathElementNotFoundError:\n            return False\n\n    def long_click(self):\n        \"\"\"find element and perform long click\"\"\"\n        self.get().long_click()\n\n    def screenshot(self) -> Image.Image:\n        \"\"\"take element screenshot\"\"\"\n        el = self.get()\n        return el.screenshot()\n    \n    def __getattr__(self, key: str):\n        \"\"\"\n        In IPython console, attr:_ipython_canary_method_should_not_exist_ will be called\n        So here ignore all attr startswith _\n        \"\"\"\n        if key.startswith(\"_\"):\n            raise AttributeError(\"Invalid attr\", key)\n        if not hasattr(DeviceXMLElement, key):\n            raise AttributeError(\"Invalid attr\", key)\n        el = self.get()\n        return getattr(el, key)\n\n\nclass XMLElement(object):\n    def __init__(self, elem: etree._Element):\n        \"\"\"\n        Args:\n            elem: lxml node\n            d: uiautomator2 instance\n        \"\"\"\n        self.elem = elem\n\n    def __hash__(self):\n        return hash(self.elem)\n\n    def __eq__(self, value):\n        return self.__hash__() == hash(value)\n\n    def __repr__(self):\n        x, y = self.center()\n        return \"<XMLElement [{tag!r} center:({x}, {y})]>\".format(\n            tag=self.elem.tag, x=x, y=y\n        )\n\n    def get_xpath(self, strip_index: bool = False):\n        \"\"\"get element full xpath\"\"\"\n        root = self.elem.getroottree()\n        path = root.getpath(self.elem)\n        if strip_index:\n            path = re.sub(r\"\\[\\d+\\]\", \"\", path)  # remove indexes\n        return path\n\n    def center(self):\n        \"\"\"\n        Returns:\n            (x, y)\n        \"\"\"\n        return self.offset(0.5, 0.5)\n\n    def offset(self, px: float = 0.0, py: float = 0.0):\n        \"\"\"\n        Offset from left_top\n\n        Args:\n            px (float): percent of width\n            py (float): percent of height\n\n        Example:\n            offset(0.5, 0.5) means center\n        \"\"\"\n        x, y, width, height = self.rect\n        return x + int(width * px), y + int(height * py)\n\n    def parent(self, xpath: Optional[str] = None) -> Union[\"XMLElement\", None]:\n        \"\"\"\n        Returns parent element\n        \"\"\"\n        if xpath is None:\n            return XMLElement(self.elem.getparent())\n\n        root = self.elem.getroottree()\n        e = self.elem\n        els = []\n        while e is not None and e != root:\n            els.append(e)\n            e = e.getparent()\n\n        xpath = strict_xpath(xpath)\n        matches = root.xpath(\n            xpath, namespaces={\"re\": \"http://exslt.org/regular-expressions\"}\n        )\n        all_paths = [root.getpath(m) for m in matches]\n        for e in reversed(els):\n            if root.getpath(e) in all_paths:\n                return XMLElement(e)\n\n    @functools.cached_property\n    def bounds(self) -> Tuple[int, int, int, int]:\n        \"\"\"\n        Returns:\n            tuple of (left, top, right, bottom)\n        \"\"\"\n        bounds = self.elem.attrib.get(\"bounds\")\n        if not bounds:\n            return (0, 0, 0, 0)\n        lx, ly, rx, ry = map(int, re.findall(r\"\\d+\", bounds))\n        return (lx, ly, rx, ry)\n\n    @property\n    def rect(self) -> Tuple[int, int, int, int]:\n        \"\"\"\n        Returns:\n            (left_top_x, left_top_y, width, height)\n        \"\"\"\n        lx, ly, rx, ry = self.bounds\n        return lx, ly, rx - lx, ry - ly\n\n    @property\n    def text(self):\n        return self.elem.attrib.get(\"text\")\n\n    @property\n    def attrib(self) -> Dict[str, str]:\n        return dict(self.elem.attrib)\n    \n    @property\n    def info(self) -> Dict[str, Any]:\n        ret = {}\n        for k, v in dict(self.attrib).items():\n            if k in (\"bounds\", \"class\", \"package\", \"content-desc\"):\n                continue\n            if k in (\"checkable\", \"checked\", \"clickable\", \"enabled\", \"focusable\", \"focused\", \"scrollable\",\n                     \"long-clickable\", \"password\", \"selected\", \"visible-to-user\"):\n                ret[convert_to_camel_case(k)] = v == \"true\"\n            elif k == \"index\":\n                ret[k] = int(v)\n            else:\n                ret[convert_to_camel_case(k)] = v\n\n        ret[\"childCount\"] = len(self.elem.getchildren())\n        ret[\"className\"] = self.elem.tag\n        lx, ly, rx, ry = self.bounds\n        ret[\"bounds\"] = {\"left\": lx, \"top\": ly, \"right\": rx, \"bottom\": ry}\n\n        # 名字命名的有点奇怪，为了兼容性暂时保留\n        ret[\"packageName\"] = self.attrib.get(\"package\")\n        ret[\"contentDescription\"] = self.attrib.get(\"content-desc\")\n        ret[\"resourceName\"] = self.attrib.get(\"resource-id\")\n        return ret\n    \n\nclass DeviceXMLElement(XMLElement):\n    def __init__(self, el: XMLElement, parent: XPathEntry):\n        super().__init__(el.elem)\n        self._parent = parent\n\n    def click(self):\n        \"\"\"\n        click element, 100ms between down and up\n        \"\"\"\n        x, y = self.center()\n        self._parent._d.click(x, y)\n\n    def long_click(self):\n        \"\"\"\n        Sometime long click is needed, 400ms between down and up\n        \"\"\"\n        x, y = self.center()\n        self._parent._d.long_click(x, y)\n\n    def screenshot(self):\n        \"\"\"\n        Take screenshot of element\n        \"\"\"\n        im = self._parent._d.screenshot()\n        return im.crop(self.bounds)\n\n    def swipe(self, direction: Union[Direction, str], scale: float = 0.6):\n        \"\"\"\n        Args:\n            direction: one of [\"left\", \"right\", \"up\", \"down\"]\n            scale: percent of swipe, range (0, 1.0)\n\n        Raises:\n            AssertionError, ValueError\n        \"\"\"\n        return swipe_in_bounds(self._parent._d, self.bounds, direction, scale)\n\n    def scroll(self, direction: Union[Direction, str] = Direction.FORWARD) -> bool:\n        \"\"\"\n        Args:\n            direction: Direction eg: Direction.FORWARD\n\n        Returns:\n            bool: if can be scroll again\n        \"\"\"\n        if direction == \"forward\":\n            direction = Direction.FORWARD\n        elif direction == \"backward\":\n            direction = Direction.BACKWARD\n\n        els = set(self._parent(\"//*\").all())\n        self.swipe(direction, scale=0.6)\n\n        # check if there is more element\n        new_elements = set(self._parent(\"//*\").all()) - els\n        ppath = self.get_xpath() + \"/\"  # limit to child nodes\n        els = [el for el in new_elements if el.get_xpath().startswith(ppath)]\n        return len(els) > 0\n\n    def scroll_to(\n        self, xpath: str, direction: Direction = Direction.FORWARD, max_swipes: int = 10\n    ) -> Union[\"XMLElement\", None]:\n        assert max_swipes > 0\n        target = self._parent(xpath)\n        for i in range(max_swipes):\n            if target.exists:\n                return target.get_last_match()\n            if not self.scroll(direction):\n                break\n        return None\n\n    def percent_bounds(self, wsize: Optional[tuple] = None):\n        \"\"\"\n        Args:\n            wsize (tuple(int, int)): window size\n\n        Returns:\n            list of 4 float, eg: 0.1, 0.2, 0.5, 0.8\n        \"\"\"\n        lx, ly, rx, ry = self.bounds\n        ww, wh = wsize or self._parent._d.window_size()\n        return (lx / ww, ly / wh, rx / ww, ry / wh)\n\n    def percent_size(self):\n        \"\"\"Returns:\n        (float, float): eg, (0.5, 0.5) means 50%, 50%\n        \"\"\"\n        ww, wh = self._parent._d.window_size()\n        _, _, w, h = self.rect\n        return (w / ww, h / wh)"
  },
  {
    "path": "uibox/LICENSE",
    "content": ""
  },
  {
    "path": "uibox/Makefile",
    "content": "build:\n\t@go work use .\n\t@GOOS=linux GOARCH=arm64 go build -o uibox main.go\n\n\n# Define color codes\nGREEN := \\033[32m\nYELLOW := \\033[33m\nRESET := \\033[0m\n\nADB_ARGS := \"-d\"\n\ndefine echo\n    @echo \">>> $(GREEN)$(1)$(RESET)\"\nendef\n\n\ntest: build\n\t@adb $(ADB_ARGS) shell getprop ro.product.cpu.abilist\n\t@$(call echo,Pushing uibox to /data/local/tmp/uibox)\n\t@adb $(ADB_ARGS) push uibox /data/local/tmp/uibox\n\t@adb $(ADB_ARGS) shell chmod 777 /data/local/tmp/uibox\n\t@$(call echo,Running uibox)\n\t@adb $(ADB_ARGS) shell /data/local/tmp/uibox -h"
  },
  {
    "path": "uibox/README.md",
    "content": "# uibox\n\nAdd new command\n\n```\ncobra-init add nohup\n```"
  },
  {
    "path": "uibox/cmd/httpcheck.go",
    "content": "/*\nCopyright © 2024 NAME HERE <EMAIL ADDRESS>\n*/\npackage cmd\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/spf13/cobra\"\n)\n\n// httpcheckCmd represents the httpcheck command\nvar httpcheckCmd = &cobra.Command{\n\tUse:   \"httpcheck\",\n\tShort: \"check if network is available by sending a http request\",\n\tRun:   httpcheckRun,\n}\n\nfunc init() {\n\trootCmd.AddCommand(httpcheckCmd)\n\n\thttpcheckCmd.Flags().StringP(\"dns\", \"d\", \"114.114.114.114\", \"DNS resolver\")\n}\n\nfunc httpcheckRun(_ *cobra.Command, args []string) {\n\tfmt.Println(\"httpcheck called\")\n\tdoHttpCheck()\n}\n\n// Function to check a single site with specific DNS and timeout settings\nfunc checkSite(url string, wg *sync.WaitGroup, resultChan chan<- bool, dnsResolver string, httpTimeout time.Duration) {\n\tdefer wg.Done()\n\n\t// Append default DNS port if not specified\n\tif !strings.Contains(dnsResolver, \":\") {\n\t\tdnsResolver += \":53\"\n\t}\n\n\t// Custom dialer with specific DNS resolver\n\tdialer := &net.Dialer{\n\t\tTimeout: httpTimeout,\n\t\tResolver: &net.Resolver{\n\t\t\tPreferGo: true,\n\t\t\tDial: func(ctx context.Context, network, address string) (net.Conn, error) {\n\t\t\t\td := net.Dialer{\n\t\t\t\t\tTimeout: httpTimeout,\n\t\t\t\t}\n\t\t\t\treturn d.DialContext(ctx, \"udp\", dnsResolver)\n\t\t\t},\n\t\t},\n\t}\n\n\t// HTTP client using the custom transport with dialer\n\tclient := http.Client{\n\t\tTimeout: httpTimeout,\n\t\tTransport: &http.Transport{\n\t\t\tDialContext: dialer.DialContext,\n\t\t\tTLSClientConfig: &tls.Config{\n\t\t\t\tInsecureSkipVerify: true, // Skip SSL certificate verification\n\t\t\t},\n\t\t},\n\t}\n\n\t// Perform HTTP GET request\n\tresp, err := client.Get(url)\n\tif err != nil {\n\t\tfmt.Println(\"Error checking:\", url, err)\n\t\tresultChan <- false\n\t\treturn\n\t}\n\tdefer resp.Body.Close()\n\n\t// Check HTTP status\n\tif resp.StatusCode == http.StatusOK {\n\t\tfmt.Println(url, \"is up.\")\n\t\tresultChan <- true\n\t} else {\n\t\tfmt.Println(url, \"status code:\", resp.StatusCode)\n\t\tresultChan <- false\n\t}\n}\n\nfunc doHttpCheck() {\n\tvar wg sync.WaitGroup\n\tresultChan := make(chan bool, 3) // Buffer for 3 results\n\n\t// Command-line flags\n\tvar dnsResolver string = \"114.114.114.114\"\n\tvar timeoutSec int = 3\n\tvar urls = []string{\"https://taobao.com\", \"https://qq.com\", \"https://baidu.com\", \"https://www.example.com\"}\n\n\t// Convert timeout to time.Duration\n\thttpTimeout := time.Duration(timeoutSec) * time.Second\n\n\t// URLs to check\n\tfor _, url := range urls {\n\t\twg.Add(1)\n\t\tgo checkSite(url, &wg, resultChan, dnsResolver, httpTimeout)\n\t}\n\n\tgo func() {\n\t\twg.Wait()\n\t\tclose(resultChan)\n\t}()\n\n\t// Evaluate results\n\tnetworkOK := false\n\tfor result := range resultChan {\n\t\tif result {\n\t\t\tnetworkOK = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif networkOK {\n\t\tfmt.Println(\"network=true\")\n\t} else {\n\t\tfmt.Println(\"network=false\")\n\t}\n}\n"
  },
  {
    "path": "uibox/cmd/nohup.go",
    "content": "/*\nCopyright © 2024 NAME HERE <EMAIL ADDRESS>\n*/\npackage cmd\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"syscall\"\n\n\t\"github.com/spf13/cobra\"\n)\n\n// nohupCmd represents the nohup command\nvar nohupCmd = &cobra.Command{\n\tUse: \"nohup\",\n\n\tShort: \"invoke a utility immune to hangups\",\n\tLong: `The nohup utility invokes utility with its arguments and at this time sets the signal SIGHUP to be ignored.\nIf the standard output is a terminal, the standard output is appended to the file nohup.out in the current directory.\nIf standard error is a terminal, it is directed to the same place as the standard output.`,\n\tDisableFlagParsing: true,\n\tRun:                nohupRun,\n}\n\nfunc init() {\n\trootCmd.AddCommand(nohupCmd)\n\n\tnohupCmd.SetUsageTemplate(`Usage: nohup COMMAND [ARG]...`)\n}\n\nfunc nohupRun(_ *cobra.Command, args []string) {\n\t// Ignore SIGHUP signal\n\tcmd := exec.Command(args[0], args[1:]...)\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\tcmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}\n\n\t// Start the command\n\tif err := cmd.Start(); err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"Error starting command: %v\\n\", err)\n\t\tos.Exit(1)\n\t}\n\n\t// Wait for the command to finish\n\tif err := cmd.Wait(); err != nil {\n\t\tfmt.Fprintf(os.Stderr, \"Error waiting for command: %v\\n\", err)\n\t\tos.Exit(1)\n\t}\n}\n"
  },
  {
    "path": "uibox/cmd/root.go",
    "content": "/*\nCopyright © 2024 NAME HERE <EMAIL ADDRESS>\n\n*/\npackage cmd\n\nimport (\n\t\"os\"\n\n\t\"github.com/spf13/cobra\"\n)\n\n\n\n// rootCmd represents the base command when called without any subcommands\nvar rootCmd = &cobra.Command{\n\tUse:   \"uibox\",\n\tShort: \"A brief description of your application\",\n\tLong: `A longer description that spans multiple lines and likely contains\nexamples and usage of using your application. For example:\n\nCobra is a CLI library for Go that empowers applications.\nThis application is a tool to generate the needed files\nto quickly create a Cobra application.`,\n\t// Uncomment the following line if your bare application\n\t// has an action associated with it:\n\t// Run: func(cmd *cobra.Command, args []string) { },\n}\n\n// Execute adds all child commands to the root command and sets flags appropriately.\n// This is called by main.main(). It only needs to happen once to the rootCmd.\nfunc Execute() {\n\terr := rootCmd.Execute()\n\tif err != nil {\n\t\tos.Exit(1)\n\t}\n}\n\nfunc init() {\n\t// Here you will define your flags and configuration settings.\n\t// Cobra supports persistent flags, which, if defined here,\n\t// will be global for your application.\n\n\t// rootCmd.PersistentFlags().StringVar(&cfgFile, \"config\", \"\", \"config file (default is $HOME/.uibox.yaml)\")\n\n\t// Cobra also supports local flags, which will only run\n\t// when this action is called directly.\n\trootCmd.Flags().BoolP(\"toggle\", \"t\", false, \"Help message for toggle\")\n}\n\n\n"
  },
  {
    "path": "uibox/go.mod",
    "content": "module github.com/openatx/uiautomator2/uibox\n\ngo 1.22.1\n\nrequire github.com/spf13/cobra v1.8.0\n\nrequire (\n\tgithub.com/inconshreveable/mousetrap v1.1.0 // indirect\n\tgithub.com/spf13/pflag v1.0.5 // indirect\n)\n"
  },
  {
    "path": "uibox/go.sum",
    "content": "github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=\ngithub.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=\ngithub.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=\ngithub.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=\ngithub.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=\ngithub.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=\ngithub.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "uibox/go.work",
    "content": "go 1.22.1\n\nuse .\n"
  },
  {
    "path": "uibox/main.go",
    "content": "/*\nCopyright © 2024 NAME HERE <EMAIL ADDRESS>\n*/\npackage main\n\nimport \"github.com/openatx/uiautomator2/uibox/cmd\"\n\nfunc main() {\n\tcmd.Execute()\n}\n"
  }
]