├── .github └── workflows │ └── python-publish.yml ├── LICENSE ├── MANIFEST.in ├── README.md ├── adbutils ├── __init__.py ├── _utils.py ├── _wraps.py ├── adb.py ├── constant.py ├── exceptions.py ├── extra │ ├── __init__.py │ ├── apk │ │ └── __init__.py │ ├── minicap │ │ ├── __init__.py │ │ └── exceptions.py │ ├── minitouch │ │ └── __init__.py │ ├── performance │ │ ├── __init__.py │ │ ├── cpu.py │ │ ├── exceptions.py │ │ ├── fps.py │ │ └── meminfo.py │ └── rotation │ │ └── __init__.py └── static │ ├── ADBKeyboard.apk │ ├── aapt2 │ ├── arm64-v8a │ │ ├── bin │ │ │ └── aapt2 │ │ ├── lib │ │ │ └── libaapt2.so │ │ └── lib64 │ │ │ └── libaapt2.so │ ├── armeabi-v7a │ │ ├── bin │ │ │ └── aapt2 │ │ └── lib │ │ │ └── libaapt2.so │ ├── x86 │ │ ├── bin │ │ │ └── aapt2 │ │ └── lib │ │ │ └── libaapt2.so │ └── x86_64 │ │ ├── bin │ │ └── aapt2 │ │ ├── lib │ │ └── libaapt2.so │ │ └── lib64 │ │ └── libaapt2.so │ ├── adb │ ├── linux │ │ └── adb │ ├── linux_arm │ │ └── adb │ ├── mac │ │ └── adb │ └── windows │ │ ├── AdbWinApi.dll │ │ ├── AdbWinUsbApi.dll │ │ └── adb.exe │ ├── busyBox │ ├── busybox-armv5l │ ├── busybox-armv7l │ ├── busybox-armv7m │ ├── busybox-armv7r │ └── busybox-armv8l │ └── stf_libs │ ├── arm64-v8a │ ├── minicap │ ├── minicap-nopie │ ├── minitouch │ └── minitouch-nopie │ ├── armeabi-v7a │ ├── minicap │ ├── minicap-nopie │ ├── minitouch │ └── minitouch-nopie │ ├── armeabi │ ├── minitouch │ └── minitouch-nopie │ ├── minicap-shared │ └── aosp │ │ └── libs │ │ ├── android-10 │ │ └── armeabi-v7a │ │ │ └── minicap.so │ │ ├── android-14 │ │ ├── armeabi-v7a │ │ │ └── minicap.so │ │ └── x86 │ │ │ └── minicap.so │ │ ├── android-15 │ │ ├── armeabi-v7a │ │ │ └── minicap.so │ │ └── x86 │ │ │ └── minicap.so │ │ ├── android-16 │ │ ├── armeabi-v7a │ │ │ └── minicap.so │ │ └── x86 │ │ │ └── minicap.so │ │ ├── android-17 │ │ ├── armeabi-v7a │ │ │ └── minicap.so │ │ └── x86 │ │ │ └── minicap.so │ │ ├── android-18 │ │ ├── armeabi-v7a │ │ │ └── minicap.so │ │ └── x86 │ │ │ └── minicap.so │ │ ├── android-19 │ │ ├── armeabi-v7a │ │ │ └── minicap.so │ │ └── x86 │ │ │ └── minicap.so │ │ ├── android-21 │ │ ├── arm64-v8a │ │ │ └── minicap.so │ │ ├── armeabi-v7a │ │ │ └── minicap.so │ │ ├── x86 │ │ │ └── minicap.so │ │ └── x86_64 │ │ │ └── minicap.so │ │ ├── android-22 │ │ ├── arm64-v8a │ │ │ └── minicap.so │ │ ├── armeabi-v7a │ │ │ └── minicap.so │ │ ├── x86 │ │ │ └── minicap.so │ │ └── x86_64 │ │ │ └── minicap.so │ │ ├── android-23 │ │ ├── arm64-v8a │ │ │ └── minicap.so │ │ ├── armeabi-v7a │ │ │ └── minicap.so │ │ ├── x86 │ │ │ └── minicap.so │ │ └── x86_64 │ │ │ └── minicap.so │ │ ├── android-24 │ │ ├── arm64-v8a │ │ │ └── minicap.so │ │ ├── armeabi-v7a │ │ │ └── minicap.so │ │ ├── x86 │ │ │ └── minicap.so │ │ └── x86_64 │ │ │ └── minicap.so │ │ ├── android-25 │ │ ├── arm64-v8a │ │ │ └── minicap.so │ │ ├── armeabi-v7a │ │ │ └── minicap.so │ │ ├── x86 │ │ │ └── minicap.so │ │ └── x86_64 │ │ │ └── minicap.so │ │ ├── android-26 │ │ ├── arm64-v8a │ │ │ └── minicap.so │ │ ├── armeabi-v7a │ │ │ └── minicap.so │ │ ├── x86 │ │ │ └── minicap.so │ │ └── x86_64 │ │ │ └── minicap.so │ │ ├── android-27 │ │ ├── arm64-v8a │ │ │ └── minicap.so │ │ ├── armeabi-v7a │ │ │ └── minicap.so │ │ ├── x86 │ │ │ └── minicap.so │ │ └── x86_64 │ │ │ └── minicap.so │ │ ├── android-28 │ │ ├── arm64-v8a │ │ │ └── minicap.so │ │ ├── armeabi-v7a │ │ │ └── minicap.so │ │ ├── x86 │ │ │ └── minicap.so │ │ └── x86_64 │ │ │ └── minicap.so │ │ ├── android-29 │ │ ├── arm64-v8a │ │ │ └── minicap.so │ │ ├── armeabi-v7a │ │ │ └── minicap.so │ │ ├── x86 │ │ │ └── minicap.so │ │ └── x86_64 │ │ │ └── minicap.so │ │ ├── android-30 │ │ ├── arm64-v8a │ │ │ └── minicap.so │ │ ├── armeabi-v7a │ │ │ └── minicap.so │ │ ├── x86 │ │ │ └── minicap.so │ │ └── x86_64 │ │ │ └── minicap.so │ │ └── android-9 │ │ └── armeabi-v7a │ │ └── minicap.so │ ├── mips │ ├── minitouch │ └── minitouch-nopie │ ├── mips64 │ ├── minitouch │ └── minitouch-nopie │ ├── x86 │ ├── minicap │ ├── minicap-nopie │ ├── minitouch │ └── minitouch-nopie │ └── x86_64 │ ├── minicap │ ├── minicap-nopie │ ├── minitouch │ └── minitouch-nopie ├── main.py ├── requirements.txt └── setup.py /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Upload Python Package 10 | 11 | on: 12 | release: 13 | types: [published] 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | deploy: 20 | 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - uses: actions/checkout@v3 25 | - name: Set up Python 26 | uses: actions/setup-python@v3 27 | with: 28 | python-version: '3.x' 29 | - name: Install dependencies 30 | run: | 31 | python -m pip install --upgrade pip 32 | pip install build 33 | - name: Build package 34 | run: python -m build 35 | - name: Publish package 36 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 37 | with: 38 | user: __token__ 39 | password: ${{ secrets.PYPI_API_TOKEN }} 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include requirements.txt 2 | recursive-include adbutils/static * 3 | include adbutils/static/*/*/* 4 | include adbutils/static/*/* 5 | include adbtuils/static/stf_libs/minicap-shared/aosp/libs/*/*/*.* 6 | 7 | include adbtuils/extra * 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # adb-utils 2 | ![PyPI - Downloads](https://img.shields.io/pypi/dm/adb-utils) 3 | 4 | 对adb常用接口的二次开发 5 | 6 | ## Requirements 7 | - python>=3.8 8 | - 项目里用到了[base_image](https://github.com/hakaboom/base_image) 因此需要额外安装opencv-python 9 | 10 | 11 | ## Installation 12 | pip3 install adb-utils 13 | 14 | ## TODO 15 | - [ ] minicap集成 16 | - [ ] minitouch集成 17 | - [ ] 设备性能数据可视化 18 | - [ ] 基于PYQT的设备管理工具 19 | 20 | ## 函数 21 | 不打算写,看源码备注就行了 22 | -------------------------------------------------------------------------------- /adbutils/__init__.py: -------------------------------------------------------------------------------- 1 | from adbutils.adb import ADBClient, ADBDevice 2 | 3 | 4 | __all__ = ['ADBDevice', 'ADBClient'] 5 | 6 | -------------------------------------------------------------------------------- /adbutils/_utils.py: -------------------------------------------------------------------------------- 1 | import platform 2 | import sys 3 | import os 4 | import queue 5 | import socket 6 | import threading 7 | import subprocess 8 | from typing import IO, Optional, Union 9 | from adbutils.constant import DEFAULT_ADB_PATH 10 | 11 | 12 | def check_file(fileName: str): 13 | """check file in path""" 14 | return os.path.isfile(f'{fileName}') 15 | 16 | 17 | def get_std_encoding(stream): 18 | """ 19 | Get encoding of the stream 20 | Args: 21 | stream: stream 22 | Returns: 23 | encoding or file system encoding 24 | """ 25 | return getattr(stream, "encoding", None) or sys.getfilesystemencoding() 26 | 27 | 28 | def split_cmd(cmds) -> list: 29 | """ 30 | Split the commands to the list for subprocess 31 | Args: 32 | cmds (str): command 33 | Returns: 34 | command list 35 | """ 36 | # cmds = shlex.split(cmds) # disable auto removing \ on windows 37 | return cmds.split() if isinstance(cmds, str) else list(cmds) 38 | 39 | 40 | def _popen_kwargs() -> dict: 41 | creationflags = 0 42 | startupinfo = None 43 | if sys.platform.startswith('win'): 44 | try: 45 | creationflags = subprocess.CREATE_NO_WINDOW # python 3.7+ 46 | except AttributeError: 47 | creationflags = 0x8000000 48 | return { 49 | 'creationflags': creationflags, 50 | 'startupinfo': startupinfo, 51 | } 52 | 53 | 54 | def get_adb_exe() -> str: 55 | """ 56 | 获取adb路径 57 | :return: 58 | """ 59 | # find in $PATH 60 | cmds = ['adb', "--version"] 61 | try: 62 | with open(os.devnull, "w") as null: 63 | subprocess.check_call( 64 | cmds, stdout=null, stderr=subprocess.STDOUT, **_popen_kwargs() 65 | ) 66 | adb_path = 'adb' 67 | except (FileNotFoundError, OSError, ValueError): 68 | system = platform.system() 69 | machine = platform.machine() 70 | adb_path = DEFAULT_ADB_PATH.get(f'{system}-{machine}') 71 | if not adb_path: 72 | adb_path = DEFAULT_ADB_PATH.get(f'{system}') 73 | if not adb_path: 74 | raise RuntimeError(f"No adb executable supports this platform({system}-{machine}).") 75 | 76 | return adb_path 77 | 78 | 79 | class NonBlockingStreamReader(object): 80 | # TODO: 增加一个方法用于非阻塞状态将stream输出存入文件 81 | def __init__(self, stream: IO, raise_EOF: Optional[bool] = False, print_output: bool = True, 82 | print_new_line: bool = True): 83 | self._s = stream 84 | self._q = queue.Queue() 85 | self._lastline = None 86 | self.name = id(self) 87 | 88 | def _populateQueue(_stream: IO, _queue: queue.Queue, kill_event: threading.Event): 89 | """ 90 | Collect lines from 'stream' and put them in 'queue' 91 | 92 | Args: 93 | _stream: 文件流 94 | _queue: 队列 95 | kill_event: 一个事件管理标志 96 | 97 | Returns: 98 | None 99 | """ 100 | while not kill_event.is_set(): 101 | line = _stream.readline() 102 | if line is not None: 103 | _queue.put(line) 104 | if print_output: 105 | if print_new_line and line == self._lastline: 106 | continue 107 | self._lastline = line 108 | elif kill_event.is_set(): 109 | break 110 | elif raise_EOF: 111 | raise UnexpectedEndOfStream 112 | else: 113 | break 114 | 115 | self._kill_event = threading.Event() 116 | self._t = threading.Thread(target=_populateQueue, args=(self._s, self._q, self._kill_event)) 117 | self._t.daemon = True 118 | self._t.start() # start collecting lines from the stream 119 | 120 | def readline(self, timeout: Union[int] = None): 121 | try: 122 | return self._q.get(block=timeout is not None, timeout=timeout) 123 | except queue.Empty: 124 | return None 125 | 126 | def read(self) -> bytes: 127 | lines = [] 128 | while True: 129 | line = self.readline() 130 | if line is None: 131 | break 132 | lines.append(line) 133 | return b"".join(lines) 134 | 135 | def kill(self) -> None: 136 | self._kill_event.set() 137 | 138 | 139 | class UnexpectedEndOfStream(Exception): 140 | pass 141 | 142 | 143 | CLEANUP_CALLS = queue.Queue() 144 | 145 | 146 | def reg_cleanup(func, *args, **kwargs): 147 | """ 148 | Clean the register for given function 149 | Args: 150 | func: function name 151 | *args: optional argument 152 | **kwargs: optional arguments 153 | Returns: 154 | None 155 | """ 156 | CLEANUP_CALLS.put((func, args, kwargs)) 157 | 158 | 159 | class SafeSocket(object): 160 | """safe and exact recv & send""" 161 | def __init__(self, sock=None): 162 | if sock is None: 163 | self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 164 | else: 165 | self.sock = sock 166 | self.buf = b"" 167 | 168 | # PEP 3113 -- Removal of Tuple Parameter Unpacking 169 | # https://www.python.org/dev/peps/pep-3113/ 170 | def connect(self, tuple_hp): 171 | host, port = tuple_hp 172 | self.sock.connect((host, port)) 173 | 174 | def send(self, msg): 175 | totalsent = 0 176 | while totalsent < len(msg): 177 | sent = self.sock.send(msg[totalsent:]) 178 | if sent == 0: 179 | raise socket.error("socket connection broken") 180 | totalsent += sent 181 | 182 | def recv(self, size): 183 | while len(self.buf) < size: 184 | trunk = self.sock.recv(min(size-len(self.buf), 4096)) 185 | if trunk == b"": 186 | raise socket.error("socket connection broken") 187 | self.buf += trunk 188 | ret, self.buf = self.buf[:size], self.buf[size:] 189 | return ret 190 | 191 | def recv_with_timeout(self, size, timeout=2): 192 | self.sock.settimeout(timeout) 193 | try: 194 | ret = self.recv(size) 195 | except socket.timeout: 196 | ret = None 197 | finally: 198 | self.sock.settimeout(None) 199 | return ret 200 | 201 | def recv_nonblocking(self, size): 202 | self.sock.settimeout(0) 203 | try: 204 | ret = self.recv(size) 205 | except socket.error as e: 206 | # 10035 no data when nonblocking 207 | if e.args[0] == 10035: # errno.EWOULDBLOCK 208 | ret = None 209 | # 10053 connection abort by client 210 | # 10054 connection reset by peer 211 | elif e.args[0] in [10053, 10054]: # errno.ECONNABORTED: 212 | raise 213 | else: 214 | raise 215 | return ret 216 | 217 | def close(self): 218 | self.sock.close() 219 | -------------------------------------------------------------------------------- /adbutils/_wraps.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import time 3 | import threading 4 | from inspect import isfunction 5 | from functools import wraps 6 | from typing import Optional, Union, Tuple, List, Type 7 | 8 | 9 | class print_run_time(object): 10 | def __init__(self): 11 | pass 12 | 13 | def __call__(self, func): 14 | @wraps(func) 15 | def wrapped_function(*args, **kwargs): 16 | start_time = time.time() 17 | ret = func(*args, **kwargs) 18 | print("{}() run time is {time:.2f}ms".format(func.__name__, time=(time.time() - start_time) * 1000)) 19 | return ret 20 | return wrapped_function 21 | 22 | 23 | class retries(object): 24 | def __init__(self, max_tries: int, delay: Optional[int] = 1, 25 | exceptions: Tuple[Type[Exception], ...] = (Exception,), hook=None): 26 | """ 27 | 通过装饰器实现的"重试"函数 28 | 29 | Args: 30 | max_tries: 最大可重试次数。超出次数后仍然失败,则弹出异常 31 | delay: 重试等待间隔 32 | exceptions: 需要检测的异常 33 | hook: 钩子函数 34 | """ 35 | self.max_tries = max_tries 36 | self.delay = delay 37 | self.exceptions = exceptions 38 | self.hook = hook 39 | 40 | def __call__(self, func): 41 | @wraps(func) 42 | def wrapped_function(*args, **kwargs): 43 | tries = list(range(self.max_tries)) 44 | tries.reverse() 45 | for tries_remaining in tries: 46 | try: 47 | return func(*args, **kwargs) 48 | except self.exceptions as err: 49 | if tries_remaining > 0: 50 | if isfunction(self.hook): 51 | self.hook(tries_remaining, err) 52 | time.sleep(self.delay) 53 | else: 54 | raise err 55 | return wrapped_function 56 | 57 | 58 | class ThreadSafeIter: 59 | """ 60 | Takes an iterator/generator and makes it thread-safe by 61 | serializing call to the `next` method of given iterator/generator. 62 | """ 63 | def __init__(self, it): 64 | self.it = it 65 | self.lock = threading.Lock() 66 | self._next = self.it.__next__ # py3 67 | 68 | def __iter__(self): 69 | return self 70 | 71 | def __next__(self): 72 | with self.lock: 73 | return self._next() 74 | 75 | def send(self, *args): 76 | with self.lock: 77 | return self.it.send(*args) 78 | 79 | 80 | def threadsafe_generator(func): 81 | """ 82 | A decorator that takes a generator function and makes it thread-safe. 83 | """ 84 | @wraps(func) 85 | def wrapped_function(*a, **kw): 86 | return ThreadSafeIter(func(*a, **kw)) 87 | return wrapped_function 88 | -------------------------------------------------------------------------------- /adbutils/adb.py: -------------------------------------------------------------------------------- 1 | import queue 2 | import random 3 | import subprocess 4 | import re 5 | import socket 6 | import os 7 | import threading 8 | import time 9 | import warnings 10 | import numpy as np 11 | from baseImage import Rect, Point 12 | 13 | from adbutils._utils import (get_adb_exe, split_cmd, _popen_kwargs, get_std_encoding, check_file, 14 | NonBlockingStreamReader, reg_cleanup) 15 | from adbutils.constant import (ANDROID_ADB_SERVER_HOST, ANDROID_ADB_SERVER_PORT, ADB_CAP_RAW_REMOTE_PATH, 16 | ADB_CAP_RAW_LOCAL_PATH, IP_PATTERN, ADB_DEFAULT_KEYBOARD, ANDROID_TMP_PATH, 17 | ADB_KEYBOARD_APK_PATH) 18 | from adbutils.exceptions import (AdbError, AdbShellError, AdbBaseError, AdbTimeout, NoDeviceSpecifyError, 19 | AdbDeviceConnectError, AdbInstallError, AdbSDKVersionError, AdbExtraModuleNotFount) 20 | from adbutils._wraps import retries 21 | from loguru import logger 22 | 23 | from typing import Union, List, Optional, Tuple, Dict, Match, Iterator, Final, Generator, Any 24 | 25 | 26 | class ADBClient(object): 27 | SUBPROCESS_FLAG: Final[int] = _popen_kwargs()['creationflags'] 28 | 29 | def __init__(self, device_id: Optional[str] = None, adb_path: Optional[str] = None, 30 | host: Optional[str] = ANDROID_ADB_SERVER_HOST, 31 | port: Optional[int] = ANDROID_ADB_SERVER_PORT): 32 | """ 33 | Args: 34 | device_id (str): 指定设备名 35 | adb_path (str): 指定adb路径 36 | host (str): 指定连接地址 37 | port (int): 指定连接端口 38 | """ 39 | self.device_id = device_id 40 | self.adb_path = adb_path or get_adb_exe() 41 | self._set_cmd_options(host, port) 42 | self.connect() 43 | 44 | @property 45 | def host(self) -> str: 46 | return self.__host 47 | 48 | @property 49 | def port(self) -> int: 50 | return self.__port 51 | 52 | def _set_cmd_options(self, host: str, port: int): 53 | """ 54 | Args: 55 | host (str): 指定连接地址 56 | port (int): 指定连接端口 57 | """ 58 | self.__host = host 59 | self.__port = port 60 | self.cmd_options = [self.adb_path] 61 | if self.host not in ('127.0.0.1', 'localhost'): 62 | self.cmd_options += ['-H', self.host] 63 | if self.port != ANDROID_ADB_SERVER_PORT: 64 | self.cmd_options += ['-P', str(self.port)] 65 | 66 | @property 67 | def server_version(self) -> int: 68 | """ 69 | 获得cmd version 70 | 71 | Returns: 72 | adb server版本 73 | """ 74 | ret = self.cmd('version', devices=False) 75 | pattern = re.compile(r'Android Debug Bridge version \d.\d.(\d+)') 76 | if version := pattern.findall(ret): 77 | return int(version[0]) 78 | 79 | @property 80 | def devices(self) -> Dict[str, str]: 81 | """ 82 | command 'adb devices' 83 | 84 | Returns: 85 | devices dict key[device_name]-value[device_state] 86 | """ 87 | pattern = re.compile(r'([\S]+)\t([\w]+)\n?') 88 | ret = self.cmd("devices", devices=False) 89 | return {value[0]: value[1] for value in pattern.findall(ret)} 90 | 91 | def get_device_id(self, decode: bool = False) -> str: 92 | return decode and self.device_id.replace(':', '_') or self.device_id 93 | 94 | def start_server(self) -> None: 95 | """ 96 | command 'adb start_server' 97 | 98 | Returns: 99 | None 100 | """ 101 | self.cmd('start-server', devices=False) 102 | 103 | def kill_server(self) -> None: 104 | """ 105 | command 'adb kill_server' 106 | 107 | Returns: 108 | None 109 | """ 110 | self.cmd('kill-server', devices=False) 111 | 112 | @retries(2, exceptions=(AdbDeviceConnectError,)) 113 | def connect(self, force: Optional[bool] = False) -> None: 114 | """ 115 | command 'adb connect ' 116 | 117 | Args: 118 | force: 不判断设备当前状态,强制连接 119 | 120 | Returns: 121 | 连接成功返回True,连接失败返回False 122 | """ 123 | if self.device_id and ':' in self.device_id and (force or self.status != 'devices'): 124 | ret = self.cmd(f"connect {self.device_id}", devices=False, skip_error=True) 125 | if 'failed' in ret: 126 | raise AdbDeviceConnectError(f'failed to connect to {self.device_id}') 127 | 128 | def disconnect(self) -> None: 129 | """ 130 | command 'adb -s disconnect' 131 | 132 | Returns: 133 | None 134 | """ 135 | if ':' in self.device_id: 136 | self.cmd(f"disconnect {self.device_id}", devices=False) 137 | 138 | def forward(self, local: str, remote: str, no_rebind: Optional[bool] = True) -> None: 139 | """ 140 | command adb forward 141 | 142 | Args: 143 | local: 要转发的本地端口 144 | remote: 要与local绑定的设备端口 145 | no_rebind: if True,如果local端已经绑定则失败 146 | 147 | Returns: 148 | None 149 | """ 150 | cmds = ['forward'] 151 | if no_rebind: 152 | cmds += ['--no-rebind'] 153 | self.cmd(cmds + [local, remote]) 154 | 155 | def remove_forward(self, local: Optional[str] = None) -> None: 156 | """ 157 | command adb forward --remove 158 | 159 | Args: 160 | local: 本地端口。如果未指定local,则默认清除所有连接' adb forward --remove-all' 161 | 162 | Returns: 163 | None 164 | """ 165 | if local: 166 | cmds = ['forward', '--remove', local] 167 | else: 168 | cmds = ['forward', '--remove-all'] 169 | self.cmd(cmds) 170 | 171 | def get_forwards(self, device_id: Optional[str] = None) -> Dict[str, List[Tuple[str, str]]]: 172 | """ 173 | command 'adb forward --list' 174 | 175 | Args: 176 | device_id (str): 获取指定设备下的端口 177 | Returns: 178 | forwards dict key[device_name]-value[Tuple[local, remote]] 179 | """ 180 | forwards = {} 181 | pattern = re.compile(r'([\S]+)\s([\S]+)\s([\S]+)\n?') 182 | ret = self.cmd(['forward', '--list'], devices=False, skip_error=True) 183 | for value in pattern.findall(ret): 184 | if device_id and device_id != value[0]: 185 | continue 186 | if value[0] in forwards: 187 | forwards[value[0]] += [(value[1], value[2])] 188 | else: 189 | forwards[value[0]] = [(value[1], value[2])] 190 | 191 | return forwards 192 | 193 | def get_forward_port(self, remote: str, device_id: Optional[str] = None) -> Optional[int]: 194 | """ 195 | 获取开放端口的端口号 196 | 197 | Args: 198 | remote (str): 设备端口 199 | device_id (str): 获取指定设备下的端口 200 | Returns: 201 | 本地端口号 202 | """ 203 | forwards = self.get_forwards(device_id=device_id) 204 | local_pattern = re.compile(r'tcp:(\d+)') 205 | for device_id, value in forwards.items(): 206 | if isinstance(value, (list, tuple)): 207 | for _local, _remote in value: 208 | if (_remote == remote) and (ret := local_pattern.search(_local)): 209 | return int(ret.group(1)) 210 | return None 211 | 212 | def get_available_forward_local(self) -> int: 213 | """ 214 | 随机获取一个可用的端口号 215 | 216 | Returns: 217 | 可用端口(int) 218 | """ 219 | sock = socket.socket() 220 | port = random.randint(11111, 20000) 221 | result = False 222 | try: 223 | sock.bind((self.host, port)) 224 | result = True 225 | except socket.error: 226 | pass 227 | sock.close() 228 | if result: 229 | return port 230 | return self.get_available_forward_local() 231 | 232 | def push(self, local: str, remote: str) -> None: 233 | """ 234 | command 'adb push ' 235 | 236 | Args: 237 | local: 发送文件的路径 238 | remote: 发送到设备上的路径 239 | 240 | Raises: 241 | RuntimeError:文件不存在 242 | 243 | Returns: 244 | None 245 | """ 246 | if not check_file(local): 247 | raise RuntimeError(f"file: {local} does not exists") 248 | self.cmd(['push', local, remote], decode=False) 249 | 250 | def push_with_progress(self, local: str, remote: str) -> Generator[Union[str, bool], Any, None]: 251 | """ 252 | 特殊push方法, 返回一个生成器, 通过next获取push进度,push完成后返回True 253 | 254 | Args: 255 | local: 本地的路径 256 | remote: 设备上的路径 257 | 258 | Returns: 259 | 生成器 260 | """ 261 | proc = self.start_cmd(cmds=['push', local, remote]) 262 | 263 | nbsp = NonBlockingStreamReader(proc.stdout) 264 | progressRE = re.compile(r'\[\s*(\d+)%]') 265 | while True: 266 | line: bytes = nbsp.readline(timeout=1) 267 | if line is None: 268 | raise AdbBaseError(proc.stderr) 269 | elif b'file pushed' in line: 270 | break 271 | line: str = line.decode(get_std_encoding(line)) 272 | yield progressRE.search(line).group(1) 273 | 274 | yield True 275 | reg_cleanup(proc.kill) 276 | 277 | def pull(self, local: str, remote: str) -> None: 278 | """ 279 | command 'adb pull 280 | 281 | Args: 282 | local: 本地的路径 283 | remote: 设备上的路径 284 | 285 | Returns: 286 | None 287 | """ 288 | self.cmd(['pull', remote, local], decode=False) 289 | 290 | def pull_with_progress(self, local: str, remote: str) -> Generator[Union[str, bool], Any, None]: 291 | """ 292 | 特殊pull方法, 返回一个生成器, 通过next获取pull进度,pull完成后返回True 293 | 294 | Args: 295 | local: 本地的路径 296 | remote: 设备上的路径 297 | 298 | Returns: 299 | 生成器 300 | """ 301 | proc = self.start_cmd(cmds=['pull', remote, local]) 302 | 303 | nbsp = NonBlockingStreamReader(proc.stdout) 304 | progressRE = re.compile(r'\[\s*(\d+)%]') 305 | while True: 306 | line: bytes = nbsp.readline(timeout=1) 307 | if line is None: 308 | raise AdbBaseError(proc.stderr) 309 | elif b'file pulled' in line: 310 | break 311 | line: str = line.decode(get_std_encoding(line)) 312 | yield progressRE.search(line).group(1) 313 | 314 | yield True 315 | reg_cleanup(proc.kill) 316 | 317 | def install(self, local: str, install_options: Union[str, list, None] = None) -> bool: 318 | """ 319 | command 'adb install ' 320 | 321 | Args: 322 | local: apk文件路径 323 | install_options: 可指定参数 324 | "-r", # 重新安装现有应用,并保留其数据。 325 | "-t", # 允许安装测试 APK。 326 | "-g", # 授予应用清单中列出的所有权限。 327 | "-d", # 允许APK降级覆盖安装 328 | "-l", # 将应用安装到保护目录/mnt/asec 329 | "-s", # 将应用安装到sdcard 330 | Raises: 331 | AdbInstallError: 安装失败 332 | AdbError: 安装失败 333 | Returns: 334 | 安装成功返回True 335 | """ 336 | cmds = ['install'] 337 | if isinstance(install_options, str): 338 | cmds.append(install_options) 339 | elif isinstance(install_options, list): 340 | cmds += install_options 341 | 342 | cmds = cmds + [local] 343 | proc = self.start_cmd(cmds) 344 | stdout, stderr = proc.communicate() 345 | 346 | stdout = stdout.decode(get_std_encoding(stdout)) 347 | stderr = stderr.decode(get_std_encoding(stdout)) 348 | 349 | pattern = re.compile(r"Failure \[(.+):.+\]") 350 | if proc.returncode == 0: 351 | return True 352 | elif pattern.search(stderr): 353 | raise AdbInstallError(pattern.findall(stderr)[0]) 354 | else: 355 | raise AdbError(stdout, stderr) 356 | 357 | def uninstall(self, package_name: str, install_options: Optional[str] = None) -> None: 358 | """ 359 | command 'adb uninstall 360 | 361 | Args: 362 | package_name: 需要卸载的包名 363 | install_options: 可指定参数 364 | "-k", # 移除软件包后保留数据和缓存目录。 365 | 366 | Returns: 367 | None 368 | """ 369 | cmds = ['uninstall'] 370 | if install_options and isinstance(install_options, str): 371 | cmds.append(install_options) 372 | 373 | cmds = cmds + [package_name] 374 | self.cmd(cmds) 375 | 376 | @property 377 | def status(self) -> Optional[str]: 378 | """ 379 | command adb -s get-state,返回当前设备状态 380 | 381 | Returns: 382 | 当前设备状态 383 | """ 384 | proc = self.start_cmd('get-state') 385 | stdout, stderr = proc.communicate() 386 | 387 | stdout = stdout.decode(get_std_encoding(stdout)) 388 | stderr = stderr.decode(get_std_encoding(stdout)) 389 | 390 | if proc.returncode == 0: 391 | return stdout.strip() 392 | elif "not found" in stderr: 393 | return None 394 | elif 'device offline' in stderr: 395 | return 'offline' 396 | else: 397 | raise AdbError(stdout, stderr) 398 | 399 | def cmd(self, cmds: Union[list, str], devices: Optional[bool] = True, decode: Optional[bool] = True, 400 | timeout: Optional[int] = None, skip_error: Optional[bool] = False): 401 | """ 402 | 创建cmd命令, 并返回命令返回值 403 | 404 | Args: 405 | cmds (list,str): 需要运行的参数 406 | devices (bool): 如果为True,则需要指定device-id,命令中会传入-s 407 | decode (bool): 是否解码stdout,stderr 408 | timeout (int): 设置命令超时时间 409 | skip_error (bool): 是否跳过报错 410 | Raises: 411 | AdbDeviceConnectError: 设备连接异常 412 | AdbTimeout:输入命令超时 413 | Returns: 414 | 返回命令结果stdout 415 | """ 416 | 417 | proc = self.start_cmd(cmds, devices) 418 | if timeout and isinstance(timeout, int): 419 | try: 420 | stdout, stderr = proc.communicate(timeout=timeout) 421 | except subprocess.TimeoutExpired: 422 | proc.kill() 423 | _, stderr = proc.communicate() 424 | raise AdbTimeout(f"cmd command {' '.join(proc.args)} time out") 425 | else: 426 | stdout, stderr = proc.communicate() 427 | 428 | if decode: 429 | stdout = stdout.decode(get_std_encoding(stdout)) 430 | stderr = stderr.decode(get_std_encoding(stderr)) 431 | 432 | if proc.returncode > 0: 433 | pattern = AdbDeviceConnectError.CONNECT_ERROR 434 | if isinstance(stderr, bytes): 435 | pattern = pattern.encode("utf-8") 436 | if re.search(pattern, stderr): 437 | raise AdbDeviceConnectError(stderr) 438 | if not skip_error: 439 | raise AdbError(stdout, stderr) 440 | 441 | return stdout 442 | 443 | def start_cmd(self, cmds: Union[list, str], devices: bool = True) -> subprocess.Popen: 444 | """ 445 | 根据cmds创建一个Popen 446 | 447 | Args: 448 | cmds: cmd commands 449 | devices: 如果为True,则需要指定device-id,命令中会传入-s 450 | Raises: 451 | NoDeviceSpecifyError:没有指定设备运行cmd命令 452 | Returns: 453 | Popen管道 454 | """ 455 | cmds = split_cmd(cmds) 456 | if devices: 457 | if not self.device_id: 458 | raise NoDeviceSpecifyError('must set device_id') 459 | cmd_options = self.cmd_options + ['-s', self.device_id] 460 | else: 461 | cmd_options = self.cmd_options 462 | 463 | cmds = cmd_options + cmds 464 | logger.info(' '.join(cmds)) 465 | proc = subprocess.Popen( 466 | cmds, 467 | stdin=subprocess.PIPE, 468 | stdout=subprocess.PIPE, 469 | stderr=subprocess.PIPE, 470 | creationflags=self.SUBPROCESS_FLAG 471 | ) 472 | return proc 473 | 474 | 475 | class ADBShell(ADBClient): 476 | SHELL_ENCODING: Final[str] = 'utf-8' # adb shell的编码 477 | PS_HEAD: Final[List[str]] = ['user', 'pid', 'ppid', 'vsize', 'rss', '', 'wchan', 'pc', 'name'] # adb shell ps 478 | 479 | @property 480 | def line_breaker(self) -> str: 481 | """ 482 | Set carriage return and line break property for various platforms and SDK versions 483 | 484 | Returns: 485 | carriage return and line break string 486 | """ 487 | if not hasattr(self, '_line_breaker'): 488 | if self.sdk_version >= 24: 489 | line_breaker = os.linesep 490 | else: 491 | line_breaker = '\r' + os.linesep 492 | line_breaker = line_breaker.encode("ascii") 493 | setattr(self, '_line_breaker', line_breaker) 494 | 495 | return getattr(self, '_line_breaker') 496 | 497 | @property 498 | def memory(self) -> str: 499 | """ 500 | 获取设备内存大小 501 | 502 | Returns: 503 | 单位MB 504 | """ 505 | ret = self.shell(['dumpsys', 'meminfo']) 506 | pattern = re.compile(r'.*Total RAM:\s+(\S+)\s+', re.DOTALL) 507 | if m := pattern.search(ret): 508 | memory = m.group(1) 509 | else: 510 | raise AdbBaseError(ret) 511 | 512 | if ',' in memory: 513 | memory = memory.split(',') 514 | # GB: memory[0], MB: memory[1], KB: memory[2] 515 | memory = int(int(memory[0]) * 1024) + int(memory[1]) 516 | else: 517 | memory = round(int(memory) / 1024) 518 | return f'{str(memory)}MB' 519 | 520 | @property 521 | def cpu_coreNum(self) -> Optional[int]: 522 | """ 523 | 获取cpu核心数量 524 | 525 | Returns: 526 | cpu核心数量 527 | """ 528 | if not hasattr(self, '_cpu_coreNum'): 529 | setattr(self, '_cpu_coreNum', int(self.shell("cat /proc/cpuinfo").strip().count('processor'))) 530 | 531 | return getattr(self, '_cpu_coreNum') 532 | 533 | @property 534 | def cpu_max_freq(self) -> List[Optional[int]]: 535 | """ 536 | 获取cpu各核心的最高频率 537 | 538 | Raises: 539 | AdbBaseError: 获取cpu信息失败 540 | Returns: 541 | 包含核心最高频率的列表 542 | """ 543 | _cores = [] 544 | cmds = [f"cat /sys/devices/system/cpu/cpu{i}/cpufreq/scaling_max_freq" for i in range(self.cpu_coreNum)] 545 | cmds = '&'.join(cmds) 546 | 547 | ret = self.shell(cmds) 548 | if not ret: 549 | raise AdbBaseError('get cpufreq error') 550 | 551 | pattern = re.compile('(\d+)') 552 | if ret := pattern.findall(ret): 553 | _cores = [int(int(core) / 1000) for core in ret] 554 | else: 555 | raise AdbBaseError('get cpufreq error') 556 | 557 | return _cores 558 | 559 | @property 560 | def cpu_min_freq(self) -> List[Optional[int]]: 561 | """ 562 | 获取cpu各核心的最低频率 563 | 564 | Raises: 565 | AdbBaseError: 获取cpu信息失败 566 | Returns: 567 | 包含核心最低频率的列表 568 | """ 569 | _cores = [] 570 | cmds = [f"cat /sys/devices/system/cpu/cpu{i}/cpufreq/scaling_min_freq" for i in range(self.cpu_coreNum)] 571 | cmds = '&'.join(cmds) 572 | 573 | ret = self.shell(cmds) 574 | if not ret: 575 | raise AdbBaseError('get cpufreq error') 576 | 577 | pattern = re.compile('(\d+)') 578 | if ret := pattern.findall(ret): 579 | _cores = [int(int(core) / 1000) for core in ret] 580 | else: 581 | raise AdbBaseError('get cpufreq error') 582 | 583 | return _cores 584 | 585 | @property 586 | def cpu_cur_freq(self) -> List[Optional[int]]: 587 | """ 588 | 获取cpu各核心的当前频率 589 | 590 | Raises: 591 | AdbBaseError: 获取cpu信息失败 592 | Returns: 593 | 包含核心当前频率的列表 594 | """ 595 | _cores = [] 596 | cmds = [f"cat /sys/devices/system/cpu/cpu{i}/cpufreq/scaling_cur_freq" for i in range(self.cpu_coreNum)] 597 | cmds = '&'.join(cmds) 598 | 599 | ret = self.shell(cmds) 600 | if not ret: 601 | raise AdbBaseError('get cpufreq error') 602 | 603 | pattern = re.compile('(\d+)') 604 | if ret := pattern.findall(ret): 605 | _cores = [int(int(core) / 1000) for core in ret] 606 | else: 607 | raise AdbBaseError('get cpufreq error') 608 | 609 | return _cores 610 | 611 | @property 612 | def cpu_abi(self) -> str: 613 | """ 614 | 获取cpu构架 615 | 616 | Returns: 617 | cpu构建 618 | """ 619 | if not hasattr(self, '_cpu_adi'): 620 | setattr(self, '_cpu_adi', self.shell("getprop ro.product.cpu.abi").strip()) 621 | 622 | return getattr(self, '_cpu_adi') 623 | 624 | @property 625 | def gpu_model(self): 626 | """ 627 | 获取gpu型号 628 | 629 | Returns: 630 | gpu型号 631 | """ 632 | if not hasattr(self, '_gpu_model'): 633 | ret = self.shell('dumpsys SurfaceFlinger') 634 | pattern = re.compile(r'GLES:\s+(.*)') 635 | m = pattern.search(ret) 636 | if not m: 637 | return None 638 | _list = m.group(1).split(',') 639 | gpuModel = '' 640 | 641 | if len(_list) > 0: 642 | gpuModel = _list[1].strip() 643 | setattr(self, '_gpu_model', gpuModel) 644 | 645 | return getattr(self, '_gpu_model') 646 | 647 | @property 648 | def opengl_version(self): 649 | """ 650 | 获取设备opengl版本 651 | 652 | Returns: 653 | opengl版本 654 | """ 655 | if not hasattr(self, '_opengl'): 656 | ret = self.shell('dumpsys SurfaceFlinger') 657 | pattern = re.compile(r'GLES:\s+(.*)') 658 | m = pattern.search(ret) 659 | if not m: 660 | return None 661 | _list = m.group(1).split(',') 662 | opengl = '' 663 | 664 | if len(_list) > 1: 665 | m2 = re.search(r'(\S+\s+\S+\s+\S+).*', _list[2]) 666 | if m2: 667 | opengl = m2.group(1) 668 | setattr(self, '_opengl', opengl) 669 | 670 | return getattr(self, '_opengl') 671 | 672 | @property 673 | def model(self) -> str: 674 | """ 675 | 获取手机型号 676 | 677 | Returns: 678 | 手机型号 679 | """ 680 | if not hasattr(self, '_model'): 681 | setattr(self, '_model', self.getprop('ro.product.model')) 682 | 683 | return getattr(self, '_model') 684 | 685 | @property 686 | def manufacturer(self) -> str: 687 | """ 688 | 获取手机厂商名 689 | 690 | Returns: 691 | 手机厂商名 692 | """ 693 | if not hasattr(self, '_manufacturer'): 694 | setattr(self, '_manufacturer', self.getprop('ro.product.manufacturer')) 695 | 696 | return getattr(self, '_manufacturer') 697 | 698 | @property 699 | def android_version(self) -> str: 700 | """ 701 | 获取系统安卓版本 702 | 703 | Returns: 704 | 安卓版本 705 | """ 706 | if not hasattr(self, '_android_version'): 707 | setattr(self, '_android_version', self.getprop('ro.build.version.release')) 708 | 709 | return getattr(self, '_android_version') 710 | 711 | @property 712 | def sdk_version(self) -> int: 713 | """ 714 | 获取sdk版本 715 | 716 | Returns: 717 | sdk版本号 718 | """ 719 | if not hasattr(self, '_sdk_version'): 720 | setattr(self, '_sdk_version', int(self.getprop('ro.build.version.sdk'))) 721 | 722 | return getattr(self, '_sdk_version') 723 | 724 | @property 725 | def abi_version(self) -> str: 726 | """ 727 | 获取abi版本 728 | 729 | Returns: 730 | abi版本 731 | """ 732 | if not hasattr(self, '_abi_version'): 733 | setattr(self, '_abi_version', self.getprop('ro.product.cpu.abi')) 734 | 735 | return getattr(self, '_abi_version') 736 | 737 | @property 738 | def displayInfo(self) -> Dict[str, Union[int, float]]: 739 | """ 740 | 获取屏幕数据 741 | 742 | Returns: 743 | width/height/density/orientation/rotation/max_x/max_y 744 | """ 745 | display_info = self.getPhysicalDisplayInfo() 746 | orientation = self.orientation 747 | max_x, max_y = self.getMaxXY() 748 | display_info.update({ 749 | "orientation": orientation, 750 | "rotation": orientation * 90, 751 | "max_x": max_x, 752 | "max_y": max_y, 753 | }) 754 | return display_info 755 | 756 | @property 757 | def dpi(self) -> int: 758 | if ret := self.getprop('ro.sf.lcd_density', True): 759 | return int(ret) 760 | 761 | @property 762 | def orientation(self) -> int: 763 | """ 764 | 获取屏幕方向 765 | 766 | Returns: 767 | 屏幕方向 0/1/2 768 | """ 769 | return self.getDisplayOrientation() 770 | 771 | @property 772 | def ip_address(self) -> Optional[str]: 773 | """ 774 | 获得设备ip地址 775 | 776 | Returns: 777 | 未找到则返回None,找到则返回IP address 778 | """ 779 | 780 | def get_ip_address_from_interface(interface): 781 | # android >= 6.0: ip -f inet addr show {interface} 782 | try: 783 | res = self.shell(f'ip -f inet addr show {interface}') 784 | except AdbShellError: 785 | res = '' 786 | if matcher := re.search(r"inet (?P(\d+\.){3}\d+)", res): 787 | return matcher.group('ip') 788 | 789 | # android >= 6.0 backup method: ifconfig 790 | try: 791 | res = self.shell('ifconfig') 792 | except AdbShellError: 793 | res = '' 794 | if matcher := re.search(interface + r'.*?inet addr:((\d+\.){3}\d+)', res, re.DOTALL): 795 | return matcher.group(1) 796 | 797 | # android <= 6.0: netcfg 798 | try: 799 | res = self.shell('netcfg') 800 | except AdbShellError: 801 | res = '' 802 | if matcher := re.search(interface + r'.* ((\d+\.){3}\d+)/\d+', res): 803 | return matcher.group(1) 804 | 805 | # android <= 6.0 backup method: getprop dhcp.{}.ipaddress 806 | try: 807 | res = self.shell('getprop dhcp.{}.ipaddress'.format(interface)) 808 | except AdbShellError: 809 | res = '' 810 | if matcher := IP_PATTERN.search(res): 811 | return matcher.group(0) 812 | 813 | # sorry, no more methods... 814 | return None 815 | 816 | interfaces = ('eth0', 'eth1', 'wlan0') 817 | for i in interfaces: 818 | ip = get_ip_address_from_interface(i) 819 | if ip and not ip.startswith('172.') and not ip.startswith('127.') and not ip.startswith('169.'): 820 | return ip 821 | return None 822 | 823 | @property 824 | def foreground_activity(self) -> str: 825 | """ 826 | 获取当前前台activity 827 | 828 | Raises: 829 | AdbBaseError: 没有获取到前台activity 830 | Returns: 831 | 当前activity 832 | """ 833 | if m := self._get_activityRecord(key='mResumedActivity'): 834 | return m.group('activity') 835 | else: 836 | raise AdbBaseError(f'running_activities get None') 837 | 838 | @property 839 | def foreground_package(self) -> str: 840 | """ 841 | 获取当前前台包名 842 | 843 | Raises: 844 | AdbBaseError: 没有获取到前台包名 845 | Returns: 846 | 当前包名 847 | """ 848 | if m := self._get_activityRecord(key='mResumedActivity'): 849 | return m.group('packageName') 850 | else: 851 | raise AdbBaseError(f'running_activities get None') 852 | 853 | @property 854 | def running_activities(self) -> List[str]: 855 | """ 856 | 获取正在运行的所有activity 857 | 858 | Raises: 859 | AdbBaseError: 未获取到当前运行的activity 860 | Returns: 861 | 所有正在运行的activity 862 | """ 863 | if m := self._get_running_activities(): 864 | return [match.group('activity') for match in m] 865 | else: 866 | raise AdbBaseError(f'running_activities get None') 867 | 868 | @property 869 | def running_package(self) -> List[str]: 870 | """ 871 | 获取正在运行的所有包名 872 | 873 | Raises: 874 | AdbBaseError: 未获取到当前运行的包名 875 | Returns: 876 | 所有正在运行的包名 877 | """ 878 | if m := self._get_running_activities(): 879 | return [match.group('packageName') for match in m] 880 | else: 881 | raise AdbBaseError(f'running_activities get None') 882 | 883 | @property 884 | def default_ime(self) -> str: 885 | """ 886 | 获取当前输入法ID 887 | 888 | Returns: 889 | 输入法ID 890 | """ 891 | try: 892 | ime = self.shell(['settings', 'get', 'secure', 'default_input_method']).strip() 893 | except AdbShellError: 894 | ime = None 895 | return ime 896 | 897 | @property 898 | def ime_list(self) -> List[str]: 899 | """ 900 | 获取系统可用输入法 901 | 902 | Returns: 903 | 输入法列表 904 | """ 905 | if ret := self.shell(['ime', 'list', '-s']): 906 | return ret.split() 907 | 908 | def is_keyboard_shown(self) -> bool: 909 | """ 910 | 判断键盘是否显示 911 | 912 | Returns: 913 | True显示键盘/False未显示键盘 914 | """ 915 | 916 | if ret := self.shell(['dumpsys', 'input_method']): 917 | return 'mInputShown=true' in ret 918 | return False 919 | 920 | def is_screenon(self) -> bool: 921 | """ 922 | 检测屏幕打开关闭状态 923 | 924 | Raises: 925 | AdbBaseError: 未检测到屏幕打开状态 926 | Returns: 927 | True屏幕打开/False屏幕关闭 928 | """ 929 | pattern = re.compile(r'mScreenOnFully=(?Ptrue|false)') 930 | ret = self.shell(['dumpsys', 'window', 'policy']) 931 | 932 | if m := pattern.search(ret): 933 | return m.group('Bool') == 'true' 934 | else: 935 | # MIUI11 936 | screenOnRE = re.compile('screenState=(SCREEN_STATE_ON|SCREEN_STATE_OFF)') 937 | m = screenOnRE.search(self.shell('dumpsys window policy')) 938 | if m: 939 | return m.group(1) == 'SCREEN_STATE_ON' 940 | raise AdbBaseError('Could not determine screen ON state') 941 | 942 | def is_locked(self) -> bool: 943 | """ 944 | 判断屏幕是否锁定 945 | 946 | Raises: 947 | AdbBaseError: 未检测到屏幕锁定状态 948 | Returns: 949 | True屏幕锁定/False屏幕未锁定 950 | """ 951 | ret = self.shell('dumpsys window policy') 952 | pattern = re.compile(r'(?:mShowingLockscreen|isStatusBarKeyguard|showing)=(?Ptrue|false)') 953 | 954 | if m := pattern.search(ret): 955 | return m.group('Bool') == 'true' 956 | raise AdbBaseError('Could not determine screen lock state') 957 | 958 | def get_pid_by_name(self, packageName: str, fuzzy_search: Optional[bool] = False) -> List[Tuple[int, str]]: 959 | """ 960 | 根据进程名获取对应pid 961 | 962 | Args: 963 | packageName: 包名 964 | fuzzy_search: if True,返回所有包含packageName的pid 965 | Returns: 966 | 获取到的进程列表(pid, name) 967 | """ 968 | ps_len = len(self.PS_HEAD) 969 | if fuzzy_search: 970 | return [(int(proc[self.PS_HEAD.index('pid')]), proc_name) for proc in self.get_process() 971 | if len(proc) == ps_len and (packageName in (proc_name := proc[self.PS_HEAD.index('name')]))] 972 | else: 973 | return [(int(proc[self.PS_HEAD.index('pid')]), proc_name) for proc in self.get_process() 974 | if len(proc) == ps_len and (packageName == (proc_name := proc[self.PS_HEAD.index('name')]))] 975 | 976 | def get_process(self, flag_options: Union[str, list, tuple, None] = None) -> List[List[str]]: 977 | """ 978 | command 'adb shell ps' 979 | 980 | Returns: 981 | 所有进程的列表 982 | """ 983 | cmds = ['ps'] 984 | if isinstance(flag_options, str): 985 | cmds.append(flag_options) 986 | elif isinstance(flag_options, (list, tuple)): 987 | cmds = cmds + flag_options 988 | 989 | process = [] 990 | process_pattern = re.compile('(\S+)') 991 | if ret := self.shell(cmds): 992 | ret = ret.splitlines() 993 | for v in ret[1:]: 994 | if proc := process_pattern.findall(v): 995 | process.append(proc) 996 | 997 | return process 998 | 999 | def _get_running_activities(self) -> Optional[List[Match[str]]]: 1000 | """ 1001 | command 'adb dumpsys activity activities' 1002 | 获取各个Stack中正在运行的activities参数 1003 | 1004 | Returns: 1005 | 包含了多个Match的列表, Match可以使用memory/user/packageName/activity/task 1006 | """ 1007 | running_activities = [] 1008 | cmds = ['dumpsys', 'activity', 'activities'] 1009 | activities = self.shell(cmds) 1010 | # 获取Stack 1011 | pattern = re.compile(r'Stack #([\d]+):') 1012 | stack = pattern.findall(activities) 1013 | if not stack: 1014 | return None 1015 | stack.sort() 1016 | # 根据Stack拆分running activities 1017 | for index in stack: 1018 | pattern = re.compile(rf'Stack #{index}[\s\S]+?Running activities \(most recent first\):([\s\S]+?)\r\n\r\n') 1019 | ret = pattern.findall(activities) 1020 | if ret: 1021 | running_activities.append(ret[0]) 1022 | 1023 | ret = [] 1024 | pattern = re.compile( 1025 | r"TaskRecord[\s\S]+?Run #(?P\d+):[\s]?" 1026 | r"ActivityRecord\{(?P.*) (?P.*) (?P.*)/(?P\.?.*) (?P.*)}") 1027 | for v in running_activities: 1028 | if m := pattern.search(v): 1029 | ret.append(m) 1030 | 1031 | return ret 1032 | 1033 | def _get_activityRecord(self, key: str) -> Optional[Match[str]]: 1034 | """ 1035 | command 'adb dumpsys activity activities' 1036 | 根据获取对应ActivityRecord信息 1037 | 1038 | Returns: 1039 | Match,可以使用memory/user/packageName/activity/task 1040 | """ 1041 | cmds = ['dumpsys', 'activity', 'activities'] 1042 | ret = self.shell(cmds) 1043 | pattern = re.compile( 1044 | rf'{key}: ' 1045 | r'ActivityRecord\{(?P.*) (?P.*) (?P.*)/\.?(?P.*) (?P.*)}[\n\r]') 1046 | 1047 | if m := pattern.search(ret): 1048 | return m 1049 | else: 1050 | return None 1051 | 1052 | def check_app(self, name: str) -> bool: 1053 | """ 1054 | 判断应用是否安装 1055 | 1056 | Args: 1057 | name: package name 1058 | 1059 | Returns: 1060 | return True if find, false otherwise 1061 | """ 1062 | if name in self.app_list(): 1063 | return True 1064 | return False 1065 | 1066 | def check_file(self, path: str, name: str) -> bool: 1067 | """ 1068 | command 'adb shell find in the ' 1069 | 1070 | Args: 1071 | path: 在设备上的路径 1072 | name: 需要检索的文件名 1073 | 1074 | Returns: 1075 | bool 是否找到文件 1076 | """ 1077 | return bool(self.raw_shell(['find', path, '-name', name])) 1078 | 1079 | def check_dir(self, path: str, name: str, flag: bool = False) -> bool: 1080 | """ 1081 | command 'adb shell cd 1082 | 1083 | Args: 1084 | path: 在设备上的路径 1085 | name: 需要检索的文件夹名 1086 | flag: 如果为True,则会在找不到文件夹时候创建一个新的 1087 | 1088 | Returns: 1089 | bool 是否存在路径 1090 | """ 1091 | if not bool(self.raw_shell(['find', path, '-maxdepth 1', '-type d', '-name', name])): 1092 | if flag: 1093 | self.create_dir(path=path, name=name) 1094 | return False 1095 | 1096 | return True 1097 | 1098 | def create_dir(self, path: str, name: str): 1099 | """ 1100 | command 'adb shell mkdir 1101 | 1102 | Args: 1103 | path: 在设备上的路径 1104 | name: 需要创建的文件夹名 1105 | 1106 | Returns: 1107 | None 1108 | """ 1109 | self.shell(cmds=['mkdir', '-m 755', os.path.join(path, name)]) 1110 | 1111 | def get_file_size(self, remote: str) -> int: 1112 | """ 1113 | command 'adb shell du -k -s ' 获取remote路径下文件大小 1114 | 1115 | Args: 1116 | remote: 文件路径 1117 | 1118 | Returns: 1119 | 文件大小(KB) 1120 | """ 1121 | ret = self.shell(cmds=['du', '-k', '-s', remote]) 1122 | pattern = re.compile(rf'(\d+)\s*{remote}') 1123 | if m := pattern.findall(ret): 1124 | return int(m[-1]) 1125 | 1126 | def getMaxXY(self) -> Tuple[int, int]: 1127 | """ 1128 | 获取屏幕可点击的最大长宽距离 1129 | 1130 | Returns: 1131 | max_x,max_y 1132 | """ 1133 | ret = self.shell(['getevent', '-p']).split('\n') 1134 | max_x, max_y = None, None 1135 | pattern = re.compile(r'max ([0-9]+)') 1136 | for i in ret: 1137 | if i.find('0035') != -1: 1138 | if ret := pattern.findall(i): 1139 | max_x = int(ret[0]) 1140 | 1141 | if i.find('0036') != -1: 1142 | if ret := pattern.findall(i): 1143 | max_y = int(ret[0]) 1144 | return max_x, max_y 1145 | 1146 | def getPhysicalDisplayInfo(self) -> Dict[str, Union[int, float]]: 1147 | """ 1148 | Get value for display dimension and density from `mPhysicalDisplayInfo` value obtained from `dumpsys` command. 1149 | 1150 | Returns: 1151 | physical display info for dimension and density 1152 | 1153 | """ 1154 | phyDispRE = re.compile( 1155 | r'.*PhysicalDisplayInfo{(?P\d+) x (?P\d+), .*, density (?P[\d.]+).*') 1156 | ret = self.raw_shell('dumpsys display') 1157 | if m := phyDispRE.search(ret): 1158 | displayInfo = {} 1159 | for prop in ['width', 'height']: 1160 | displayInfo[prop] = int(m.group(prop)) 1161 | for prop in ['density']: 1162 | # In mPhysicalDisplayInfo density is already a factor, no need to calculate 1163 | displayInfo[prop] = float(m.group(prop)) 1164 | return displayInfo 1165 | 1166 | # This could also be mSystem or mOverscanScreen 1167 | phyDispRE = re.compile('\s*mUnrestrictedScreen=\((?P\d+),(?P\d+)\) (?P\d+)x(?P\d+)') 1168 | # This is known to work on older versions (i.e. API 10) where mrestrictedScreen is not available 1169 | dispWHRE = re.compile(r'\s*DisplayWidth=(?P\d+) *DisplayHeight=(?P\d+)') 1170 | ret = self.raw_shell('dumpsys window') 1171 | m = phyDispRE.search(ret, 0) 1172 | if not m: 1173 | m = dispWHRE.search(ret, 0) 1174 | if m: 1175 | displayInfo = {} 1176 | for prop in ['width', 'height']: 1177 | displayInfo[prop] = int(m.group(prop)) 1178 | for prop in ['density']: 1179 | d = self._getDisplayDensity(strip=True) 1180 | if d: 1181 | displayInfo[prop] = d 1182 | else: 1183 | # No available density information 1184 | displayInfo[prop] = -1.0 1185 | return displayInfo 1186 | 1187 | # gets C{mPhysicalDisplayInfo} values from dumpsys. This is a method to obtain display dimensions and density 1188 | phyDispRE = re.compile(r'Physical size: (?P\d+)x(?P\d+).*Physical density: (?P\d+)', 1189 | re.S) 1190 | ret = self.raw_shell('wm size; wm density') 1191 | 1192 | if m := phyDispRE.search(ret): 1193 | displayInfo = {} 1194 | for prop in ['width', 'height']: 1195 | displayInfo[prop] = int(m.group(prop)) 1196 | for prop in ['density']: 1197 | displayInfo[prop] = float(m.group(prop)) 1198 | return displayInfo 1199 | 1200 | return {} 1201 | 1202 | def _getDisplayDensity(self, strip=True) -> Union[float, int]: 1203 | """ 1204 | Get display density 1205 | 1206 | Args: 1207 | strip: strip the output 1208 | Returns: 1209 | display density 1210 | """ 1211 | BASE_DPI = 160.0 1212 | 1213 | if density := self.getprop('ro.sf.lcd_density', strip): 1214 | return float(density) / BASE_DPI 1215 | 1216 | if density := self.getprop('qemu.sf.lcd_density', strip): 1217 | return float(density) / BASE_DPI 1218 | return -1.0 1219 | 1220 | def getDisplayOrientation(self) -> int: 1221 | """ 1222 | Another way to get the display orientation, this works well for older devices (SDK version 15) 1223 | 1224 | Returns: 1225 | display orientation information 1226 | 1227 | """ 1228 | # another way to get orientation, for old sumsung device(sdk version 15) 1229 | SurfaceFlingerRE = re.compile(r'orientation=(\d+)') 1230 | ret = self.shell('dumpsys SurfaceFlinger') 1231 | if m := SurfaceFlingerRE.search(ret): 1232 | return int(m.group(1)) 1233 | 1234 | # Fallback method to obtain the orientation 1235 | # See https://github.com/dtmilano/AndroidViewClient/issues/128 1236 | surfaceOrientationRE = re.compile(r'SurfaceOrientation:\s+(\d+)') 1237 | ret = self.shell('dumpsys input') 1238 | if m := surfaceOrientationRE.search(ret): 1239 | return int(m.group(1)) 1240 | # We couldn't obtain the orientation 1241 | warnings.warn("Could not obtain the orientation, return 0") 1242 | return 0 1243 | 1244 | def keyevent(self, keycode: Union[str, int]) -> None: 1245 | """ 1246 | command 'adb shell input keyevent' 1247 | Args: 1248 | keycode: key code number or name 1249 | 1250 | Returns: 1251 | None 1252 | """ 1253 | self.shell(['input', 'keyevent', str(keycode)]) 1254 | 1255 | def getprop(self, key: str, strip: Optional[bool] = True) -> Optional[str]: 1256 | """ 1257 | command 'adb shell getprop 1258 | 1259 | Args: 1260 | key: 需要查询的参数 1261 | strip: 删除文本头尾空格 1262 | 1263 | Returns: 1264 | getprop获取到的参数 1265 | """ 1266 | ret = self.raw_shell(['getprop', key]) 1267 | return strip and ret.rstrip() or ret 1268 | 1269 | def get_app_install_path(self, packageName: str) -> Optional[str]: 1270 | """ 1271 | command 'adb shell pm path ' 1272 | 1273 | Args: 1274 | packageName: 需要查找的包名 1275 | 1276 | Returns: 1277 | 包安装路径 1278 | """ 1279 | if packageName in self.app_list(): 1280 | stdout = self.shell(['pm', 'path', packageName]) 1281 | if 'package:' in stdout: 1282 | return stdout.split('package:')[1].strip() 1283 | else: 1284 | return None 1285 | 1286 | def app_list(self, flag_options: Union[str, list, tuple, None] = None) -> List[str]: 1287 | """ 1288 | command 'adb shell pm list packages' 1289 | 1290 | Args: 1291 | flag_options: 可指定参数 1292 | "-f", # 查看它们的关联文件。 1293 | "-d", # 进行过滤以仅显示已停用的软件包。 1294 | "-e", # 进行过滤以仅显示已启用的软件包。 1295 | "-s", # 进行过滤以仅显示系统软件包。 1296 | "-3", # 进行过滤以仅显示第三方软件包。 1297 | "-i", # 查看软件包的安装程序。 1298 | "-u", # 也包括已卸载的软件包。 1299 | "--user user_id", # 要查询的用户空间。 1300 | 1301 | Returns: 1302 | 1303 | """ 1304 | if self.sdk_version >= 24: 1305 | cmds = ['cmd', 'package', 'list', 'packages'] 1306 | else: 1307 | cmds = ['pm', 'list', 'packages'] 1308 | 1309 | if isinstance(flag_options, str): 1310 | cmds.append(flag_options) 1311 | elif isinstance(flag_options, (list, tuple)): 1312 | cmds = cmds + flag_options 1313 | ret = self.shell(cmds) 1314 | packages = ret.splitlines() 1315 | # remove all empty string; "package:xxx" -> "xxx" 1316 | packages = [p.split(":")[1] for p in packages if p] 1317 | return packages 1318 | 1319 | def broadcast(self, action: str, user: str = None) -> None: 1320 | """ 1321 | 发送广播信号 1322 | 1323 | Args: 1324 | action: 需要触发的广播行为 1325 | user: 向指定组件广播 1326 | 1327 | Returns: 1328 | None 1329 | """ 1330 | cmds = ['am', 'broadcast'] + ['-a', action] 1331 | if user: 1332 | cmds += ['-n', user] 1333 | self.start_cmd(cmds) 1334 | 1335 | def shell(self, cmds: Union[list, str], decode: Optional[bool] = True, skip_error: Optional[bool] = False) \ 1336 | -> Union[str, bytes]: 1337 | """ 1338 | command 'adb shell 1339 | 1340 | Args: 1341 | cmds (list,str): 需要运行的参数 1342 | decode (bool): 是否解码stdout,stderr 1343 | skip_error (bool): 是否跳过报错 1344 | Raises: 1345 | AdbShellError:指定shell命令时出错 1346 | Returns: 1347 | 命令返回结果 1348 | """ 1349 | if self.sdk_version < 25: 1350 | # sdk_version < 25, adb shell 不返回错误 1351 | # https://issuetracker.google.com/issues/36908392 1352 | cmds = split_cmd(cmds) + [';', 'echo', '---$?---'] 1353 | ret = self.raw_shell(cmds, decode=decode).rstrip() 1354 | if m := re.match("(.*)---(\d+)---$", ret, re.DOTALL): 1355 | stdout = m.group(1) 1356 | returncode = int(m.group(2)) 1357 | else: 1358 | warnings.warn('return code not matched') 1359 | stdout = ret 1360 | returncode = 0 1361 | 1362 | if returncode > 0: 1363 | if not skip_error: 1364 | raise AdbShellError(stdout, stderr=None) 1365 | return stdout 1366 | else: 1367 | try: 1368 | ret = self.raw_shell(cmds, decode=decode, skip_error=skip_error) 1369 | except AdbError as err: 1370 | raise AdbShellError(err.stdout, err.stderr) 1371 | else: 1372 | return ret 1373 | 1374 | def raw_shell(self, cmds: Union[list, str], decode: Optional[bool] = True, skip_error: Optional[bool] = False) \ 1375 | -> str: 1376 | """ 1377 | command 'adb shell 1378 | 1379 | Args: 1380 | cmds (list): 需要运行的参数 1381 | decode (bool): 是否解码stdout,stderr 1382 | skip_error (bool): 是否跳过报错 1383 | Returns: 1384 | 命令返回结果 1385 | """ 1386 | cmds = ['shell'] + split_cmd(cmds) 1387 | stdout = self.cmd(cmds, decode=False, skip_error=skip_error) 1388 | if not decode: 1389 | return stdout 1390 | 1391 | try: 1392 | return stdout.decode(self.SHELL_ENCODING) 1393 | except UnicodeDecodeError: 1394 | return str(repr(stdout)) 1395 | 1396 | def start_shell(self, cmds: Union[list, str]): 1397 | cmds = ['shell'] + split_cmd(cmds) 1398 | return self.start_cmd(cmds) 1399 | 1400 | 1401 | class ADBDevice(ADBShell): 1402 | def __init__(self, device_id: Optional[str] = None, adb_path: Optional[str] = None, 1403 | host: Optional[str] = ANDROID_ADB_SERVER_HOST, 1404 | port: Optional[int] = ANDROID_ADB_SERVER_PORT): 1405 | """ 1406 | Args: 1407 | device_id (str): 指定设备名 1408 | adb_path (str): 指定adb路径 1409 | host (str): 指定连接地址 1410 | port (int): 指定连接端口 1411 | """ 1412 | super(ADBDevice, self).__init__(device_id=device_id, adb_path=adb_path, host=host, port=port) 1413 | self.set_input_method(ime_method=ADB_DEFAULT_KEYBOARD, ime_apk_path=ADB_KEYBOARD_APK_PATH) 1414 | 1415 | def screenshot(self, rect: Union[Rect, Tuple[int, int, int, int], List[int]] = None) -> np.ndarray: 1416 | """ 1417 | command 'adb screencap' 1418 | 1419 | Args: 1420 | rect: 自定义截取范围 Rect/(x, y, width, height) 1421 | 1422 | Raises: 1423 | ValueError:传入参数rect错误 1424 | OverflowError:rect超出屏幕边界范围 1425 | Returns: 1426 | 图像数据 1427 | """ 1428 | remote_path = ADB_CAP_RAW_REMOTE_PATH 1429 | raw_local_path = ADB_CAP_RAW_LOCAL_PATH.format(device_id=self.get_device_id(True)) 1430 | 1431 | self.raw_shell(['screencap', remote_path]) 1432 | self.start_shell(['chmod', '755', remote_path]) 1433 | self.pull(local=raw_local_path, remote=remote_path) 1434 | 1435 | # read size 1436 | img_data = np.fromfile(raw_local_path, dtype=np.uint16) 1437 | width, height = img_data[2], img_data[0] 1438 | _data = img_data 1439 | # read raw 1440 | _line = 4 # 色彩通道数 1441 | img_data = np.fromfile(raw_local_path, dtype=np.uint8) 1442 | img_data = img_data[slice(_line * 3, len(img_data))] 1443 | # 范围截取 1444 | img_data = img_data.reshape(width, height, _line) 1445 | width, height = img_data.shape[1::-1] 1446 | if rect: 1447 | if isinstance(rect, Rect): 1448 | pass 1449 | elif isinstance(rect, (tuple, list)): 1450 | try: 1451 | rect = Rect(*rect) 1452 | except TypeError: 1453 | raise ValueError('param "rect" takes 4 positional arguments ') 1454 | else: 1455 | raise ValueError('param "rect" must be /tuple/list') 1456 | 1457 | # 判断边界是否超出width,height 1458 | if not Rect(0, 0, width, height).contains(rect): 1459 | raise OverflowError(f'rect不能超出屏幕边界 {rect}') 1460 | x_min, y_min = int(rect.tl.x), int(rect.tl.y) 1461 | x_max, y_max = int(rect.br.x), int(rect.br.y) 1462 | img_data = img_data[y_min:y_max, x_min:x_max] 1463 | 1464 | img_data = img_data[:, :, ::-1][:, :, 1:4] # imgData中rgbA转为ABGR,并截取bgr 1465 | # 删除raw临时文件 1466 | os.remove(raw_local_path) 1467 | return img_data 1468 | 1469 | def start_app(self, package: str, activity: Optional[str] = None): 1470 | """ 1471 | if not activity command 'adb shell monkey' 1472 | if activity command 'adb shell am start 1473 | 1474 | Args: 1475 | package: package name 1476 | activity: activity name 1477 | 1478 | Returns: 1479 | None 1480 | """ 1481 | if not activity: 1482 | cmds = ['monkey', '-p', package, '-c', 'android.intent.category.LAUNCHER', '1'] 1483 | else: 1484 | cmds = ['am', 'start', '-n', f'{package}/{package}.{activity}'] 1485 | self.shell(cmds) 1486 | 1487 | def stop_app(self, package: str) -> None: 1488 | """ 1489 | command 'adb shell am force-stop' to force stop the application 1490 | 1491 | Args: 1492 | package: package name 1493 | 1494 | Returns: 1495 | None 1496 | """ 1497 | self.shell(['am', 'force-stop', package]) 1498 | 1499 | def clear_app(self, package: str) -> None: 1500 | """ 1501 | command 'adb shell pm clear' to force stop the application 1502 | 这条命令的效果相当于在设置里的应用信息界面点击了「清除缓存」和「清除数据」 1503 | 1504 | Args: 1505 | package: package name 1506 | 1507 | Returns: 1508 | None 1509 | """ 1510 | self.shell(['pm', 'clear', package]) 1511 | 1512 | def install(self, local: str, install_options: Union[str, list, None] = None) -> bool: 1513 | """ 1514 | push apk 文件到 /data/local/tmp; 1515 | 调用 pm install 安装; 1516 | 删除 /data/local/tmp 下的对应 apk 文件 1517 | 1518 | Args: 1519 | local: apk文件路径 1520 | install_options: 可指定参数 1521 | "-r", # 重新安装现有应用,并保留其数据。 1522 | "-t", # 允许安装测试 APK。 1523 | "-g", # 授予应用清单中列出的所有权限。 1524 | "-d", # 允许APK降级覆盖安装 1525 | "-l", # 将应用安装到保护目录/mnt/asec 1526 | "-s", # 将应用安装到sdcard 1527 | Raises: 1528 | AdbInstallError: 安装失败 1529 | AdbError: 安装失败 1530 | Returns: 1531 | 安装成功返回True 1532 | """ 1533 | apk_name = os.path.split(local)[-1] 1534 | remote = os.path.join(ANDROID_TMP_PATH, apk_name) 1535 | self.push(local=local, remote=remote) 1536 | try: 1537 | flag = self.pm_install(remote=remote, install_options=install_options) 1538 | self.raw_shell(f'rm -r {remote}') 1539 | return flag 1540 | except AdbBaseError as err: 1541 | raise err 1542 | 1543 | def pm_install(self, remote: str, install_options: Union[str, list, None] = None) -> bool: 1544 | """ 1545 | command 'adb shell pm install ' 1546 | 1547 | Args: 1548 | remote: apk文件路径 1549 | install_options: 可指定参数 1550 | "-r", # 重新安装现有应用,并保留其数据。 1551 | "-t", # 允许安装测试 APK。 1552 | "-g", # 授予应用清单中列出的所有权限。 1553 | "-d", # 允许APK降级覆盖安装 1554 | "-l", # 将应用安装到保护目录/mnt/asec 1555 | "-s", # 将应用安装到sdcard 1556 | Raises: 1557 | AdbInstallError: 安装失败 1558 | AdbError: 安装失败 1559 | Returns: 1560 | 安装成功返回True 1561 | """ 1562 | cmds = ['pm', 'install'] 1563 | if isinstance(install_options, str): 1564 | cmds.append(install_options) 1565 | elif isinstance(install_options, list): 1566 | cmds += install_options 1567 | 1568 | cmds = cmds + [remote] 1569 | proc = self.start_shell(cmds) 1570 | stdout, stderr = proc.communicate() 1571 | 1572 | stdout = stdout.decode(get_std_encoding(stdout)) 1573 | stderr = stderr.decode(get_std_encoding(stdout)) 1574 | 1575 | if proc.returncode == 0: 1576 | return True 1577 | elif err := re.compile(r"Failure \[(.+):.+\]").search(stderr): 1578 | raise AdbInstallError(err.group(1)) 1579 | else: 1580 | raise AdbError(stdout, stderr) 1581 | 1582 | def tap(self, point: Union[Tuple[int, int], Point]): 1583 | """ 1584 | command 'adb shell input tap' 点击屏幕 1585 | 1586 | Args: 1587 | point: 坐标(x,y) 1588 | 1589 | Returns: 1590 | None 1591 | """ 1592 | x, y = None, None 1593 | if isinstance(point, Point): 1594 | x, y = point.x, point.y 1595 | elif isinstance(point, (tuple, list)): 1596 | x, y = point[0], point[1] 1597 | self.shell(f'input tap {x} {y}') 1598 | 1599 | def swipe(self, start_point: Union[Tuple[int, int], Point], end_point: Union[Tuple[int, int], Point], 1600 | duration: int = 500) -> None: 1601 | """ 1602 | command 'adb shell input swipe> 滑动屏幕 1603 | 1604 | Args: 1605 | start_point: 起点坐标 1606 | end_point: 重点坐标 1607 | duration: 操作后延迟 1608 | Returns: 1609 | None 1610 | """ 1611 | 1612 | def _handle(point): 1613 | if isinstance(point, Point): 1614 | return point.x, point.y 1615 | elif isinstance(point, (tuple, list)): 1616 | return point 1617 | 1618 | start_x, start_y = _handle(start_point) 1619 | end_x, end_y = _handle(end_point) 1620 | 1621 | version = self.sdk_version 1622 | if version <= 15: 1623 | raise AdbSDKVersionError(f'swipe: API <= 15 not supported (version={version})') 1624 | elif version <= 17: 1625 | self.shell(f'input swipe {start_x} {start_y} {end_x} {end_y} {duration}') 1626 | else: 1627 | self.shell(f'input touchscreen swipe {start_x} {start_y} {end_x} {end_y}') 1628 | 1629 | def set_input_method(self, ime_method: str, ime_apk_path: Optional[str] = None) -> None: 1630 | """ 1631 | 设置输入法 1632 | 1633 | Args: 1634 | ime_method: 输入法ID 1635 | ime_apk_path: 输入法安装包 1636 | Returns: 1637 | None 1638 | """ 1639 | if ime_method not in self.ime_list: 1640 | if ime_apk_path: 1641 | self.install(ime_apk_path) 1642 | if self.default_ime != ime_method: 1643 | self.shell(['ime', 'enable', ime_method]) 1644 | self.shell(['ime', 'set', ime_method]) 1645 | 1646 | def text(self, text, enter: Optional[bool] = False): 1647 | """ 1648 | input text on the device 1649 | 预置命令: #CLEAR# 清除当前输入框内所有字符。在使用原生input时不能保证百分百清空输入框数据 1650 | 1651 | Args: 1652 | text: 需要输入的字符 1653 | enter: press 'Enter' key 1654 | 1655 | Returns: 1656 | None 1657 | """ 1658 | if self.default_ime == ADB_DEFAULT_KEYBOARD: 1659 | if text == '#CLEAR#': 1660 | self.broadcast('ADB_CLEAR_TEXT') 1661 | else: 1662 | self.shell(f"am broadcast -a ADB_INPUT_TEXT --es msg '{str(text)}'") 1663 | else: 1664 | if text == '#CLEAR#': 1665 | logger.warning('建议使用AdbKeyboard') 1666 | for i in range(255): 1667 | self.keyevent('KEYCODE_CLEAR') 1668 | else: 1669 | self.shell(['input', 'text', str(text)]) 1670 | 1671 | if enter: 1672 | time.sleep(1) 1673 | self.keyevent('ENTER') 1674 | 1675 | 1676 | __all__ = ['ADBClient', 'ADBDevice'] 1677 | -------------------------------------------------------------------------------- /adbutils/constant.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | import re 4 | 5 | IP_PATTERN = re.compile(r'(\d+\.){3}\d+') 6 | THISPATH = os.path.dirname(os.path.realpath(__file__)) 7 | STATICPATH = os.path.join(THISPATH, 'static') 8 | DEFAULT_ADB_PATH = { 9 | "Windows": os.path.join(STATICPATH, "adb", "windows", 'adb'), 10 | "Darwin": os.path.join(STATICPATH, "adb", "mac", "adb"), 11 | "Linux": os.path.join(STATICPATH, "adb", "linux", "adb"), 12 | "Linux-x86_64": os.path.join(STATICPATH, "adb", "linux", "adb"), 13 | "Linux-armv7l": os.path.join(STATICPATH, "adb", "linux_arm", "adb"), 14 | } 15 | ANDROID_ADB_SERVER_HOST = '127.0.0.1' 16 | ANDROID_ADB_SERVER_PORT = 5037 17 | 18 | ANDROID_TMP_PATH = '/data/local/tmp/' 19 | ADB_CAP_RAW_REMOTE_PATH = os.path.join(ANDROID_TMP_PATH, 'screencap.raw') 20 | 21 | ADB_CAP_RAW_LOCAL_PATH = './{device_id}.raw' 22 | ADB_CAP_LOCAL_PATH = './{device_id}.png' 23 | 24 | ADB_DEFAULT_KEYBOARD = 'com.android.adbkeyboard/.AdbIME' 25 | ADB_KEYBOARD_APK_PATH = os.path.join(STATICPATH, 'ADBKeyboard.apk') 26 | 27 | ADB_INSTALL_FAILED = { 28 | "INSTALL_FAILED_ALREADY_EXISTS": "应用已经存在,或卸载了但没卸载干净;建议使用'-r'安装", 29 | "INSTALL_FAILED_INVALID_APK": "无效的 APK 文件", 30 | "INSTALL_FAILED_INVALID_URI": "无效的 APK 文件名;确保 APK 文件名里无中文", 31 | "INSTALL_FAILED_INSUFFICIENT_STORAGE": "空间不足", 32 | "INSTALL_FAILED_DUPLICATE_PACKAGE": "已经存在同名程序", 33 | "INSTALL_FAILED_NO_SHARED_USER": "请求的共享用户不存在", 34 | "INSTALL_FAILED_UPDATE_INCOMPATIBLE": "以前安装过同名应用,但卸载时数据没有移除;或者已安装该应用,但签名不一致;先 adb uninstall", 35 | "INSTALL_FAILED_SHARED_USER_INCOMPATIBLE": "请求的共享用户存在但签名不一致", 36 | "INSTALL_FAILED_MISSING_SHARED_LIBRARY": "安装包使用了设备上不可用的共享库", 37 | "INSTALL_FAILED_REPLACE_COULDNT_DELETE": "替换时无法删除", 38 | "INSTALL_FAILED_DEXOPT": "dex 优化验证失败或空间不足", 39 | "INSTALL_FAILED_OLDER_SDK": "设备系统版本低于应用要求", 40 | "INSTALL_FAILED_CONFLICTING_PROVIDER": "设备里已经存在与应用里同名的 content provider", 41 | "INSTALL_FAILED_NEWER_SDK": "设备系统版本高于应用要求", 42 | "INSTALL_FAILED_TEST_ONLY": "应用是 test-only 的,但安装时没有指定 -t 参数", 43 | "INSTALL_FAILED_CPU_ABI_INCOMPATIBLE": "包含不兼容设备 CPU 应用程序二进制接口的 native code", 44 | "INSTALL_FAILED_MISSING_FEATURE": "应用使用了设备不可用的功能", 45 | "INSTALL_FAILED_CONTAINER_ERROR": "1. sdcard 访问失败;2. 应用签名与 ROM 签名一致,被当作内置应用。", 46 | "INSTALL_FAILED_INVALID_INSTALL_LOCATION": "1. 不能安装到指定位置;2. 应用签名与 ROM 签名一致,被当作内置应用。", 47 | "INSTALL_FAILED_MEDIA_UNAVAILABLE": "安装位置不可用", 48 | "INSTALL_FAILED_VERIFICATION_TIMEOUT": "验证安装包超时", 49 | "INSTALL_FAILED_VERIFICATION_FAILURE": "验证安装包失败", 50 | "INSTALL_FAILED_PACKAGE_CHANGED": "应用与调用程序期望的不一致", 51 | "INSTALL_FAILED_UID_CHANGED": "以前安装过该应用,与本次分配的 UID 不一致", 52 | "INSTALL_FAILED_VERSION_DOWNGRADE": "已经安装了该应用更高版本", 53 | "INSTALL_FAILED_PERMISSION_MODEL_DOWNGRADE": "已安装 target SDK 支持运行时权限的同名应用,要安装的版本不支持运行时权限", 54 | "INSTALL_PARSE_FAILED_NOT_APK": "指定路径不是文件,或不是以 .apk 结尾", 55 | "INSTALL_PARSE_FAILED_BAD_MANIFEST": "无法解析的 AndroidManifest.xml 文件", 56 | "INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION": "解析器遇到异常", 57 | "INSTALL_PARSE_FAILED_NO_CERTIFICATES": "安装包没有签名", 58 | "INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES": "已安装该应用,且签名与 APK 文件不一致", 59 | "INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING": "解析 APK 文件时遇到 CertificateEncodingException", 60 | "INSTALL_PARSE_FAILED_BAD_PACKAGE_NAME": "manifest 文件里没有或者使用了无效的包名", 61 | "INSTALL_PARSE_FAILED_BAD_SHARED_USER_ID": "manifest 文件里指定了无效的共享用户 ID", 62 | "INSTALL_PARSE_FAILED_MANIFEST_MALFORMED": "解析 manifest 文件时遇到结构性错误", 63 | "INSTALL_PARSE_FAILED_MANIFEST_EMPTY": "在 manifest 文件里找不到找可操作标签(instrumentation 或 application)", 64 | "INSTALL_FAILED_INTERNAL_ERROR": "因系统问题安装失败", 65 | "INSTALL_FAILED_USER_RESTRICTED": "用户被限制安装应用", 66 | "INSTALL_FAILED_DUPLICATE_PERMISSION": "应用尝试定义一个已经存在的权限名称", 67 | "INSTALL_FAILED_NO_MATCHING_ABIS": "应用包含设备的应用程序二进制接口不支持的 native code", 68 | "INSTALL_CANCELED_BY_USER": "应用安装需要在设备上确认,但未操作设备或点了取消", 69 | "INSTALL_FAILED_ACWF_INCOMPATIBLE": "应用程序与设备不兼容", 70 | "does not contain AndroidManifest.xml": "无效的 APK 文件", 71 | "is not a valid zip file": "无效的 APK 文件", 72 | "Offline": "设备未连接成功", 73 | "unauthorized": "设备未授权允许调试", 74 | "error: device not found": "没有连接成功的设备", 75 | "protocol failure": "设备已断开连接", 76 | "Unknown option: -s": "Android 2.2 以下不支持安装到 sdcard", 77 | "No space left on device": "空间不足", 78 | "Permission denied … sdcard …": "sdcard 不可用", 79 | "signatures do not match the previously installed version; ignoring!": "已安装该应用且签名不一致", 80 | } 81 | 82 | # AAPT 83 | AAPT_LOCAL_PATH = os.path.join(STATICPATH, 'aapt', '{abi_version}', 'aapt') 84 | AAPT_REMOTE_PATH = os.path.join(ANDROID_TMP_PATH, 'aapt') 85 | 86 | # AAPT2 87 | AAPT2_LOCAL_PATH = os.path.join(STATICPATH, 'aapt2', '{abi_version}', 'bin', 'aapt2') 88 | AAPT2_REMOTE_PATH = os.path.join(ANDROID_TMP_PATH, 'aapt2') 89 | 90 | # busyBox 91 | BUSYBOX_LOCAL_PATH = os.path.join(STATICPATH, 'busybox', 'busybox-arm{}') 92 | BUSYBOX_REMOTE_PATH = os.path.join(ANDROID_TMP_PATH, 'busybox') 93 | 94 | 95 | # minicap 96 | MNC_REMOTE_PATH = os.path.join(ANDROID_TMP_PATH, 'minicap') 97 | MNC_SO_REMOTE_PATH = os.path.join(ANDROID_TMP_PATH, 'minicap.so') 98 | MNC_CMD = f'LD_LIBRARY_PATH={ANDROID_TMP_PATH} {MNC_REMOTE_PATH}' 99 | MNC_CAP_LOCAL_PATH = ADB_CAP_LOCAL_PATH 100 | MNC_LOCAL_NAME = 'minicap_{device_id}' 101 | MNC_LOCAL_PATH = os.path.join(STATICPATH, 'stf_libs', '{abi_version}', 'minicap') 102 | MNC_SO_LOCAL_PATH = os.path.join(STATICPATH, 'stf_libs', 'minicap-shared', 'aosp', 'libs', 103 | 'android-{sdk_version}', '{abi_version}', 'minicap.so') 104 | 105 | # minitouch 106 | MNT_REMOTE_PATH = os.path.join(ANDROID_TMP_PATH, 'minitouch') 107 | MNT_LOCAL_NAME = 'minitouch_{device_id}' 108 | MNT_LOCAL_PATH = os.path.join(STATICPATH, 'stf_libs', '{abi_version}', 'minitouch') 109 | -------------------------------------------------------------------------------- /adbutils/exceptions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from adbutils.constant import ADB_INSTALL_FAILED 3 | 4 | 5 | class AdbBaseError(Exception): 6 | def __init__(self, message): 7 | self.message = message 8 | 9 | def __repr__(self): 10 | return repr(self.message) 11 | 12 | 13 | class AdbError(AdbBaseError): 14 | """ There was an exception that occurred while ADB command """ 15 | def __init__(self, stdout, stderr, message: str = None): 16 | super(AdbError, self).__init__(message=message) 17 | self.stdout = stdout 18 | self.stderr = stderr 19 | 20 | def __repr__(self): 21 | return f"stdout[{self.stdout}] stderr[{self.stderr}]" 22 | 23 | 24 | class AdbShellError(AdbError): 25 | """ There was an exception that occurred while ADB shell command """ 26 | 27 | 28 | class AdbSDKVersionError(AdbBaseError): 29 | """Errors caused by insufficient sdb versions """ 30 | 31 | 32 | class AdbTimeout(AdbBaseError): 33 | """ Adb command time out""" 34 | 35 | 36 | class NoDeviceSpecifyError(AdbBaseError): 37 | """ No device was specified when ADB was commanded """ 38 | 39 | 40 | class AdbDeviceConnectError(AdbBaseError): 41 | """ Failed to connect device """ 42 | CONNECT_ERROR = r"error:\s*(" \ 43 | r"(device \'\S+\' not found)|" \ 44 | r"(cannot connect to daemon at [\w\:\s\.]+ Connection timed out)|" \ 45 | r"(device offline))" 46 | 47 | 48 | class AdbInstallError(AdbBaseError): 49 | """ An error while adb install apk failed """ 50 | def __repr__(self): 51 | return repr(str(self)) 52 | 53 | def __str__(self): 54 | if self.message in ADB_INSTALL_FAILED: 55 | return ADB_INSTALL_FAILED[self.message] 56 | else: 57 | return f'adb install failed,\n{self.message}' 58 | 59 | 60 | class AdbExtraModuleNotFount(AdbBaseError): 61 | """ An error while adb extra-module not found""" 62 | -------------------------------------------------------------------------------- /adbutils/extra/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from .apk import Apk 3 | from .minicap import Minicap 4 | from .rotation import Rotation 5 | from .performance.fps import Fps 6 | from .performance.cpu import Cpu 7 | from .performance.meminfo import Meminfo 8 | from .performance import DeviceWatcher 9 | 10 | 11 | __all__ = ['Apk', 'Minicap', 'Rotation', 'Fps', 'Cpu', 'Meminfo', 'DeviceWatcher'] -------------------------------------------------------------------------------- /adbutils/extra/apk/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from adbutils import ADBDevice 3 | from adbutils._utils import split_cmd 4 | from adbutils.constant import (AAPT_LOCAL_PATH, AAPT_REMOTE_PATH, ANDROID_TMP_PATH, 5 | BUSYBOX_REMOTE_PATH, BUSYBOX_LOCAL_PATH, 6 | AAPT2_LOCAL_PATH, AAPT2_REMOTE_PATH) 7 | from adbutils.exceptions import AdbBaseError 8 | from loguru import logger 9 | 10 | from typing import Union, Tuple, List, Optional, Match, Dict 11 | import re 12 | import os 13 | import time 14 | 15 | 16 | class Apk(object): 17 | ICON_DIR_NAME = 'icon' 18 | 19 | def __init__(self, device: ADBDevice, packageName: str): 20 | self.device = device 21 | 22 | self.install() 23 | 24 | self.packageName = packageName 25 | self.device.check_dir(path=ANDROID_TMP_PATH, name=self.ICON_DIR_NAME, flag=True) 26 | self.app_info = self._dump_app_info(packageName) 27 | 28 | @property 29 | def name(self) -> str: 30 | """ 31 | 解析获取apk的label名 32 | 33 | Returns: 34 | apk名 35 | """ 36 | if info := re.compile(r"application-label-(\S*):\'([ \S]+)\'").findall(self.app_info): 37 | for locales, name in info: 38 | if locales in ('zh', 'zh-CN', 'zh-HK'): 39 | return name 40 | if info := re.compile(r"application: label=\'(?P[ \S]+)\' icon").search(self.app_info): 41 | return info.group('app_name') 42 | 43 | @property 44 | def version_code(self) -> str: 45 | """ 46 | 解析获取apk的版本号 47 | 48 | Returns: 49 | 版本号 50 | """ 51 | if info := re.compile(r'versionCode=\'(?P\S*)\'').search(self.app_info): 52 | return info.group('versionCode') 53 | 54 | @property 55 | def version_name(self) -> str: 56 | """ 57 | 解析获取apk的版本名 58 | 59 | Returns: 60 | 版本名 61 | """ 62 | if info := re.compile(r'versionName=\'(?P.+?)\'').search(self.app_info): 63 | return info.group('versionName') 64 | 65 | @property 66 | def main_activity(self) -> str: 67 | """ 68 | 解析获取apk的主activity 69 | 70 | Returns: 71 | 主activity 72 | """ 73 | if info := re.compile(r'launchable-activity: name=\'(?P\S*)\'').search(self.app_info): 74 | return info.group('launchable_activity') 75 | 76 | @property 77 | def sdk_version(self) -> str: 78 | """ 79 | 解析获取apk的sdk版本号 80 | 81 | Returns: 82 | sdk版本号 83 | """ 84 | if info := re.compile(r'sdkVersion:\'(?P\d+)\'').search(self.app_info): 85 | return info.group('sdkVersion') 86 | 87 | @property 88 | def target_sdk_version(self) -> str: 89 | """ 90 | 解析获取apk的目标sdk版本号 91 | 92 | Returns: 93 | 目标sdk版本号 94 | """ 95 | if info := re.compile(r'targetSdkVersion:\'(?P\d+)\'').search(self.app_info): 96 | return info.group('targetSdkVersion') 97 | 98 | @property 99 | def platformBuildVersionName(self) -> str: 100 | """ 101 | 解析获取apk的构建平台版本名 102 | 103 | Returns: 104 | 构建平台版本名 105 | """ 106 | if info := re.compile(r'platformBuildVersionName=\'(?P.+?)\'').search(self.app_info): 107 | return info.group('platformBuildVersionName') 108 | 109 | @property 110 | def icon_info(self) -> str: 111 | """ 112 | 解析获取apk的icon文件信息 113 | 114 | Returns: 115 | icon文件信息 116 | """ 117 | if info := re.compile(r'application-icon-(\S*):\'(?P\S+)\'').findall(self.app_info): 118 | return info[-1][-1] 119 | 120 | @property 121 | def install_path(self) -> str: 122 | """ 123 | 获取apk安装路径 124 | 125 | Returns: 126 | 在设备上的路径 127 | """ 128 | if not hasattr(self, '_install_path'): 129 | setattr(self, '_install_path', self.device.get_app_install_path(self.packageName)) 130 | 131 | return getattr(self, '_install_path') 132 | 133 | def _dump_icon_from_androidManifest(self): 134 | xml = self.aapt_shell(['dump', 'xmltree', self.install_path, '--file', 'AndroidManifest.xml']) 135 | pattern = re.compile('E: application.*icon\(\S+\)=@(?P\S+)', re.DOTALL) 136 | if m := pattern.search(xml): 137 | return self._dump_path_from_resources(m.group('id')) 138 | 139 | def _dump_path_from_resources(self, _id: str): 140 | resources = self.aapt_shell(['dump resources', self.install_path]) 141 | pattern = re.compile(f'resource {_id}(.+?)resource', re.DOTALL) 142 | if m := pattern.search(resources): 143 | resource = m.group(1) 144 | resFileRE = re.compile('\((\S+dpi)\).* [\"\']?(\S+\.png)') 145 | if icon_file := resFileRE.findall(resource): 146 | return icon_file[-1][-1] 147 | 148 | return None 149 | 150 | def get_icon_file(self, local: str) -> None: 151 | """ 152 | 获取icon文件到本地 153 | 154 | Args: 155 | local: 需要保存到的路径 156 | 157 | Returns: 158 | None 159 | """ 160 | icon_info = self.icon_info 161 | if os.path.splitext(icon_info)[-1] == '.xml': 162 | icon_info = self._dump_icon_from_androidManifest() 163 | 164 | save_dir = os.path.join(ANDROID_TMP_PATH, f'{self.ICON_DIR_NAME}/') 165 | save_path = os.path.join(save_dir, self.packageName) 166 | # step1: 检查保存路径下是否存在包名路径 167 | self.device.check_dir(save_dir, name=self.packageName, flag=True) 168 | 169 | # step2: 解压缩base.apk里的icon文件,保存到save_path下 170 | self.device.shell(cmds=[BUSYBOX_REMOTE_PATH, 'unzip', '-oq', self.install_path, 171 | f"\"{icon_info}\"", '-d', save_path]) 172 | time.sleep(.2) 173 | 174 | # step3: 将save_path下的png文件,pull到本地 175 | pull_path = os.path.join(f'{save_path}/', icon_info) 176 | self.device.pull(remote=pull_path, local=local) 177 | 178 | def _dump_app_info(self, packageName: str) -> str: 179 | if not self.install_path: 180 | raise AdbBaseError(f"'{packageName}' install path not found") 181 | 182 | return self.aapt_shell(f'd badging {self.install_path}') 183 | 184 | def aapt_shell(self, cmds: Union[list, str]): 185 | cmds = [f'{AAPT2_REMOTE_PATH}'] + split_cmd(cmds) 186 | return self.device.shell(cmds) 187 | 188 | def _get_app_path_list(self, flag_options: Union[None, str, list] = None) -> List[Tuple[str, str]]: 189 | """ 190 | 获取app的对应地址 191 | 192 | Args: 193 | flag_options: 获取app_list可指定参数,与pm list packages相同 194 | 195 | Returns: 196 | app_path_list: tuple[app_path, packageName] 197 | """ 198 | options = ['-f'] 199 | if isinstance(flag_options, list): 200 | if '-f' not in flag_options: 201 | options += flag_options 202 | elif isinstance(flag_options, str): 203 | options += [flag_options] 204 | app_list = self.device.app_list(options) 205 | pattern = re.compile('^(\\S+)=(\\S+)$') 206 | ret = [] 207 | for app in app_list: 208 | m = pattern.findall(app) 209 | if m: 210 | ret.append(m[0]) 211 | 212 | return ret 213 | 214 | def _install_aapt(self) -> None: 215 | """ 216 | check if appt installed 217 | 218 | Returns: 219 | None 220 | """ 221 | if not self.device.check_file(ANDROID_TMP_PATH, 'aapt'): 222 | aapt_local_path = AAPT_LOCAL_PATH.format(abi_version=self.device.abi_version) 223 | self.device.push(local=aapt_local_path, remote=AAPT_REMOTE_PATH) 224 | time.sleep(1) 225 | self.device.shell(['chmod', '755', AAPT_REMOTE_PATH]) 226 | 227 | def _install_aapt2(self) -> None: 228 | """ 229 | check if appt2 installed 230 | 231 | Returns: 232 | None 233 | """ 234 | if not self.device.check_file(ANDROID_TMP_PATH, 'aapt2'): 235 | aapt2_local_path = AAPT2_LOCAL_PATH.format(abi_version=self.device.abi_version) 236 | self.device.push(local=aapt2_local_path, remote=AAPT2_REMOTE_PATH) 237 | self.device.shell(['chmod', '755', AAPT2_REMOTE_PATH]) 238 | 239 | def _install_busyBox(self) -> None: 240 | """ 241 | check if busyBox installed 242 | 243 | Returns: 244 | None 245 | """ 246 | if not self.device.check_file(ANDROID_TMP_PATH, 'busybox'): 247 | if 'v8' in self.device.abi_version: 248 | local = BUSYBOX_LOCAL_PATH.format('v8l') 249 | elif 'v7r' in self.device.abi_version: 250 | local = BUSYBOX_LOCAL_PATH.format('v7r') 251 | elif 'v7m' in self.device.abi_version: 252 | local = BUSYBOX_LOCAL_PATH.format('v7m') 253 | elif 'v7l' in self.device.abi_version: 254 | local = BUSYBOX_LOCAL_PATH.format('v7l') 255 | elif 'v5' in self.device.abi_version: 256 | local = BUSYBOX_LOCAL_PATH.format('v5l') 257 | else: 258 | local = BUSYBOX_LOCAL_PATH.format('v8l') 259 | 260 | self.device.push(local=local, remote=BUSYBOX_REMOTE_PATH) 261 | self.device.shell(['chmod', '755', BUSYBOX_REMOTE_PATH]) 262 | 263 | def install(self) -> None: 264 | """ 265 | install aapt2/busyBox 266 | 267 | Returns: 268 | None 269 | """ 270 | self._install_aapt2() 271 | self._install_busyBox() 272 | 273 | def uninstall(self) -> None: 274 | """ 275 | uninstall busyBox,aapt2 276 | 277 | Returns: 278 | None 279 | """ 280 | try: 281 | self.device.raw_shell(f'rm -r {BUSYBOX_REMOTE_PATH}') 282 | self.device.raw_shell(f'rm -f {AAPT2_REMOTE_PATH}') 283 | self.device.raw_shell(f'rm -f {AAPT_REMOTE_PATH}') 284 | except AdbBaseError as err: 285 | logger.warning(err) 286 | -------------------------------------------------------------------------------- /adbutils/extra/minicap/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import time 3 | import threading 4 | 5 | from loguru import logger 6 | from adbutils.constant import (ANDROID_TMP_PATH, MNC_REMOTE_PATH, MNC_SO_REMOTE_PATH, MNC_CMD, MNC_CAP_LOCAL_PATH, 7 | MNC_LOCAL_NAME, MNC_LOCAL_PATH, MNC_SO_LOCAL_PATH) 8 | from adbutils.extra.minicap.exceptions import MinicapStartError, MinicapServerConnectError 9 | from adbutils import ADBDevice 10 | from adbutils._utils import NonBlockingStreamReader, reg_cleanup, SafeSocket 11 | from adbutils._wraps import threadsafe_generator 12 | 13 | from typing import Tuple 14 | import struct 15 | import subprocess 16 | 17 | 18 | class Minicap(object): 19 | RECVTIMEOUT = None 20 | 21 | def __init__(self, device: ADBDevice, rotation_watcher=None): 22 | """ 23 | 初始化minicap 24 | 25 | Args: 26 | device: 设备类 27 | rotation_watcher: 方向监控函数 28 | """ 29 | self.device = device 30 | self.MNC_LOCAL_NAME = MNC_LOCAL_NAME.format(device_id=self.device.device_id) # minicap在设备上的转发名 31 | self.MNC_PORT = None # minicap在电脑上使用的端口 32 | self.quirk_flag = 0 33 | self.server_flag = False # 判断minicap服务是否启动 34 | self.proc = None 35 | self.nbsp = None 36 | 37 | self._update_rotation_event = threading.Event() 38 | if rotation_watcher: 39 | rotation_watcher.reg_callback(lambda x: self.update_rotation(x * 90)) 40 | self._install_minicap() 41 | 42 | def __str__(self): 43 | return f" port:{self.MNC_PORT}" \ 44 | f"\tlocal_name:{self.MNC_LOCAL_NAME}" 45 | 46 | def start_server(self) -> None: 47 | """ 48 | 开启minicap服务 49 | 50 | Raises: 51 | MinicapStartError: minicap server start error 52 | Returns: 53 | None 54 | """ 55 | self._set_minicap_forward() 56 | param = self._get_params() 57 | proc = self.device.start_shell([MNC_CMD, f"-n '{self.MNC_LOCAL_NAME}'", '-P', 58 | "%dx%d@%dx%d/%d 2>&1" % param]) 59 | 60 | nbsp = NonBlockingStreamReader(proc.stdout) 61 | while True: 62 | line = nbsp.readline(timeout=5) 63 | if line is None: 64 | raise MinicapStartError("minicap server setup timeout") 65 | if b'have different types' in line: 66 | raise MinicapStartError("minicap server setup error") 67 | if b"Server start" in line: 68 | logger.info('minicap server setup') 69 | break 70 | 71 | if proc.poll() is not None: 72 | raise MinicapStartError('minicap server quit immediately') 73 | reg_cleanup(proc.kill) 74 | time.sleep(.5) 75 | # self.proc = proc 76 | # self.nbsp = nbsp 77 | self.server_flag = True 78 | 79 | def teardown(self) -> None: 80 | """ 81 | 关闭minicap服务 82 | 83 | Returns: 84 | None 85 | """ 86 | logger.debug('minicap server teardown') 87 | if self.proc: 88 | self.proc.kill() 89 | 90 | if self.nbsp: 91 | self.nbsp.kill() 92 | 93 | if self.MNC_PORT and self.device.get_forward_port(remote=self.MNC_LOCAL_NAME): 94 | self.device.remove_forward(local=f'tcp:{self.MNC_PORT}') 95 | 96 | self.server_flag = False 97 | 98 | def _set_minicap_forward(self): 99 | """ 100 | 设置minicap开放的端口 101 | 102 | Returns: 103 | None 104 | """ 105 | # teardown服务后,保留端口信息,用于下次启动 106 | remote = f'localabstract:{self.MNC_LOCAL_NAME}' 107 | if port := self.device.get_forward_port(remote=remote, device_id=self.device.device_id): 108 | self.MNC_PORT = port 109 | return 110 | 111 | self.MNC_PORT = self.MNC_PORT or self.device.get_available_forward_local() 112 | self.device.forward(local=f'tcp:{self.MNC_PORT}', remote=remote) 113 | 114 | def _install_minicap(self) -> None: 115 | """ 116 | check if minicap and minicap.so installed 117 | 118 | Returns: 119 | None 120 | """ 121 | if not self.device.check_file(ANDROID_TMP_PATH, 'minicap'): 122 | self.device.push(local=MNC_LOCAL_PATH.format(abi_version=self.device.abi_version), 123 | remote=MNC_REMOTE_PATH) 124 | time.sleep(1) 125 | self.device.shell(['chmod', '755', MNC_REMOTE_PATH]) 126 | 127 | if not self.device.check_file(ANDROID_TMP_PATH, 'minicap.so'): 128 | self.device.push(local=MNC_SO_LOCAL_PATH.format(abi_version=self.device.abi_version, 129 | sdk_version=self.device.sdk_version), 130 | remote=MNC_SO_REMOTE_PATH) 131 | time.sleep(1) 132 | self.device.shell(['chmod', '755', MNC_SO_REMOTE_PATH]) 133 | 134 | def _get_params(self) -> Tuple[int, int, int, int, int]: 135 | """ 136 | 获取minicap命令需要的屏幕分辨率参数 137 | 138 | Returns: 139 | None 140 | """ 141 | display_info = self.device.displayInfo 142 | real_width = display_info['width'] 143 | real_height = display_info['height'] 144 | real_rotation = display_info['rotation'] 145 | 146 | if self.quirk_flag & 2 and real_rotation in (90, 270): 147 | params = real_height, real_width, real_height, real_width, 0 148 | else: 149 | params = real_width, real_height, real_width, real_height, real_rotation 150 | 151 | return params 152 | 153 | def update_rotation(self, rotation): 154 | """ 155 | 更新屏幕方向 156 | 157 | Args: 158 | rotation: 方向角度 159 | 160 | Returns: 161 | None 162 | """ 163 | logger.debug("minicap update_rotation: {}", rotation) 164 | self._update_rotation_event.set() 165 | 166 | def get_frame(self): 167 | """ 168 | 获取屏幕截图 169 | 170 | Returns: 171 | 图像数据 172 | """ 173 | if self._update_rotation_event.is_set(): 174 | logger.info('minicap update_rotation') 175 | self.teardown() 176 | self.start_server() 177 | self._update_rotation_event.clear() 178 | 179 | try: 180 | return self._get_frame() 181 | except (ConnectionRefusedError, OSError) as err: 182 | self.teardown() 183 | raise MinicapServerConnectError(f'{err}') 184 | 185 | def _get_frame(self): 186 | s = SafeSocket() 187 | s.connect((self.device.host, self.MNC_PORT)) 188 | t = s.recv(24) 189 | # minicap header 190 | global_headers = struct.unpack("<2B5I2B", t) 191 | # Global header binary format https://github.com/openstf/minicap#global-header-binary-format 192 | ori, self.quirk_flag = global_headers[-2:] 193 | 194 | if self.quirk_flag & 2 and ori not in (0, 1, 2): 195 | stopping = True 196 | logger.error("quirk_flag found:{}, going to resetup", self.quirk_flag) 197 | else: 198 | stopping = False 199 | 200 | if not stopping: 201 | s.send(b"1") 202 | if self.RECVTIMEOUT is not None: 203 | header = s.recv_with_timeout(4, self.RECVTIMEOUT) 204 | else: 205 | header = s.recv(4) 206 | if header is None: 207 | logger.error("minicap header is None") 208 | else: 209 | frame_size = struct.unpack(" Thread: 52 | """ 53 | 创建cpu监控线程 54 | 55 | Returns: 56 | cpu监控线程 57 | """ 58 | 59 | def _get_cpu_usage(): 60 | try: 61 | # total_cpu_usage, cpu_core_usage, app_usage_ret 62 | return self._cpu_watcher.get_cpu_usage(self._package_name) 63 | except AdbBaseError as err: 64 | logger.error(err) 65 | return None 66 | 67 | def _run(kill_event: Event, wait_event: Event, q: Queue): 68 | while not kill_event.is_set(): 69 | if not wait_event.is_set(): 70 | if cpu_usage := _get_cpu_usage(): 71 | q.put(cpu_usage) 72 | else: 73 | q.put(None) 74 | wait_event.set() 75 | 76 | _t = Thread(target=_run, name='cpu_watcher', 77 | args=(self._kill_event, self._cpu_wait_event, self._cpu_usage_queue)) 78 | _t.daemon = True 79 | return _t 80 | 81 | def create_mem_watcher(self) -> Thread: 82 | """ 83 | 创建内存监控线程 84 | 85 | Returns: 86 | 内存监控线程 87 | """ 88 | 89 | def _get_mem_usage(): 90 | try: 91 | if self._package_name: 92 | return self._mem_watcher.get_app_summary(self._package_name) 93 | else: 94 | return None 95 | except AdbBaseError as err: 96 | logger.error(err) 97 | return None 98 | 99 | def _run(kill_event: Event, wait_event: Event, q: Queue): 100 | while not kill_event.is_set(): 101 | if not wait_event.is_set(): 102 | if app_mem := _get_mem_usage(): 103 | q.put(app_mem) 104 | else: 105 | q.put(None) 106 | wait_event.set() 107 | 108 | _t = Thread(target=_run, name='mem_watcher', 109 | args=(self._kill_event, self._mem_wait_event, self._mem_usage_queue)) 110 | _t.daemon = True 111 | return _t 112 | 113 | def create_fps_watcher(self) -> Thread: 114 | """ 115 | 创建fps监控线程 116 | 117 | Returns: 118 | fps监控线程 119 | """ 120 | 121 | def _get_fps_usage(): 122 | try: 123 | if self._surfaceView_name: 124 | return self._fps_watcher.get_fps_surfaceView(f"{self._surfaceView_name}") 125 | return None 126 | except AdbBaseError as err: 127 | logger.error(err) 128 | return None 129 | 130 | def _run(kill_event: Event, wait_event: Event, q: Queue): 131 | while not kill_event.is_set(): 132 | if not wait_event.is_set(): 133 | if fps_info := _get_fps_usage(): 134 | q.put(fps_info) 135 | else: 136 | q.put(None) 137 | wait_event.set() 138 | 139 | _t = Thread(target=_run, name='mem_watcher', 140 | args=(self._kill_event, self._fps_wait_event, self._fps_usage_queue)) 141 | _t.daemon = True 142 | return _t 143 | 144 | def stop(self): 145 | self._kill_event.set() 146 | 147 | def start(self): 148 | if not self._cpu_watcher_thread.is_alive(): 149 | self._cpu_watcher_thread.start() 150 | 151 | if not self._mem_watcher_thread.is_alive(): 152 | self._mem_watcher_thread.start() 153 | 154 | if not self._fps_watcher_thread.is_alive(): 155 | self._fps_watcher.clear_surfaceFlinger_latency() 156 | self._fps_watcher_thread.start() 157 | 158 | def get(self): 159 | """ 160 | 161 | Returns: 162 | 163 | """ 164 | self._mem_wait_event.clear() 165 | self._cpu_wait_event.clear() 166 | self._fps_wait_event.clear() 167 | 168 | cpu_usage = self._cpu_usage_queue.get() 169 | mem_usage = self._mem_usage_queue.get() 170 | fps_info = self._fps_usage_queue.get() 171 | 172 | return cpu_usage, mem_usage, fps_info 173 | 174 | 175 | if __name__ == '__main__': 176 | from adbutils import ADBDevice 177 | from adbutils.extra.performance import DeviceWatcher 178 | 179 | device = ADBDevice(device_id='') 180 | a = DeviceWatcher(device, package_name=device.foreground_package) 181 | a.start() 182 | 183 | while True: 184 | start_time = time.time() 185 | cpu_usage, mem_usage, fps_info = a.get() 186 | delay_time = time.time() - start_time 187 | 188 | log = [] 189 | if cpu_usage: 190 | total_cpu_usage, cpu_core_usage, app_usage_ret = cpu_usage 191 | log.append('cpu={} core={}, {}'.format( 192 | f'{total_cpu_usage:.1f}%', 193 | '\t'.join([f'cpu{core_index}:{usage:.1f}%' for core_index, usage in enumerate(cpu_core_usage)]), 194 | '\t'.join([f'{name}:{usage:.1f}%' for name, usage in app_usage_ret.items()]), 195 | )) 196 | 197 | if fps_info: 198 | fps, fTime, jank, bigJank, _ = fps_info 199 | else: 200 | fps = fTime = 0 201 | log.append(f'fps={fps:.1f}, 最大延迟={fTime:.2f}ms') 202 | 203 | logger.debug('\t'.join(log)) 204 | if (sleep := (1 - delay_time)) > 0: 205 | time.sleep(sleep) 206 | else: 207 | time.sleep(1) 208 | -------------------------------------------------------------------------------- /adbutils/extra/performance/cpu.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import re 3 | import time 4 | from typing import Union, Tuple, List, Optional, Dict 5 | 6 | from adbutils import ADBDevice 7 | from adbutils.constant import ANDROID_TMP_PATH, BUSYBOX_LOCAL_PATH, BUSYBOX_REMOTE_PATH 8 | from adbutils._utils import get_std_encoding 9 | from adbutils.extra.performance.exceptions import AdbNoInfoReturn 10 | 11 | from loguru import logger 12 | 13 | 14 | class Cpu(object): 15 | # user/nice/system/idle/iowait/irq/softirq/stealstolen/guest 16 | cpu_jiffies_pattern = re.compile(r'(\d+)') 17 | total_cpu_pattern = re.compile(r'cpu\s+(.*)') 18 | core_stat_pattern = re.compile(r'cpu(\d+)\s*(.*)') 19 | app_stat_pattern = re.compile(r'((\d+)\s*\(\S+\)(\s+\S+){50})+') 20 | 21 | def __init__(self, device: ADBDevice): 22 | self.device = device 23 | self._total_cpu_stat = [] 24 | self._core_cpu_stat = [] 25 | self._app_cpu_stat = {} 26 | 27 | def get_cpu_usage(self, name: Union[str, int, List[Union[int, str]], Tuple[Union[str, int], ...], None] = None): 28 | """ 29 | 获取设备cpu使用率 30 | 31 | Args: 32 | name: 根据name中的值查找对应pid的cpu使用率 33 | 34 | Returns: 35 | (total_cpu_usage, core_cpu_usage, app_cpu_usage) 36 | app_cpu_usage: 是一个以pid为索引的字典 37 | core_cpu_usage: 是一个列表,索引对应cpu核心 38 | total_cpu_usage: 一个float或int 39 | """ 40 | # step1: 转换name为pid 41 | if isinstance(name, (str, int)): 42 | name = [name] 43 | pid_list = name and self._transform_name_to_pid(name) or [] 44 | 45 | # step2: 运行命令,获得cpu信息 46 | try: 47 | total_cpu_stat, core_cpu_stat, app_cpu_stat = self._get_cpu_stat(pid_list) 48 | except AdbNoInfoReturn as err: 49 | logger.warning(err) 50 | return self.get_cpu_usage(name) 51 | 52 | _return_flag = 0 53 | if not self._total_cpu_stat or not self._core_cpu_stat: 54 | self._total_cpu_stat = total_cpu_stat 55 | self._core_cpu_stat = core_cpu_stat 56 | _return_flag = 1 57 | 58 | for _name in pid_list: 59 | if not self._app_cpu_stat.get(_name): 60 | if app_stat := app_cpu_stat.get(_name): 61 | self._app_cpu_stat[_name] = app_stat 62 | _return_flag = 2 63 | 64 | if _return_flag > 0: 65 | return self.get_cpu_usage(name) 66 | 67 | # step3: 计算总使用率 68 | total_idle = total_cpu_stat[3] - self._total_cpu_stat[3] 69 | total_cpu_time = sum(total_cpu_stat) - sum(self._total_cpu_stat) 70 | total_cpu_usage = 100 * (total_cpu_time - total_idle) / total_cpu_time 71 | 72 | # step4: 计算各核心使用率 73 | cpu_core_usage = [] 74 | for cpu_index, core_stat in enumerate(core_cpu_stat): 75 | idle = core_stat[3] - self._core_cpu_stat[cpu_index][3] 76 | core_cpu_time = sum(core_stat) - sum(self._core_cpu_stat[cpu_index]) 77 | cpu_core_usage.append(100 * (core_cpu_time - idle) / core_cpu_time) 78 | 79 | self._total_cpu_stat = total_cpu_stat 80 | self._core_cpu_stat = core_cpu_stat 81 | 82 | # step5: 计算各pid使用率 83 | app_usage_ret = {} 84 | 85 | for pid, app_stat in app_cpu_stat.items(): 86 | if app_stat: 87 | app_cpu_time = sum([int(v) for v in app_stat[13:15]]) - \ 88 | sum([int(v) for v in self._app_cpu_stat[pid][13:15]]) 89 | app_usage = 100 * (app_cpu_time / total_cpu_time) 90 | app_usage_ret[name[pid_list.index(pid)]] = app_usage 91 | self._app_cpu_stat[pid] = app_stat 92 | 93 | return total_cpu_usage, cpu_core_usage, app_usage_ret 94 | 95 | def _get_cpu_stat(self, name: Optional[List[int]] = None) -> \ 96 | Tuple[List[int], List[List[int]], Dict[int, Optional[List[str]]]]: 97 | cmds = self._create_command(name) 98 | proc = self.device.start_shell(cmds) 99 | 100 | stdout, stderr = proc.communicate() 101 | 102 | stdout = stdout.decode(get_std_encoding(stdout)) 103 | stderr = stderr.decode(get_std_encoding(stdout)) 104 | 105 | app_cpu_stat = name and {pid: None for pid in name} or {} 106 | if ret := self.app_stat_pattern.findall(stdout): 107 | pattern = re.compile(r'(\S+)\s*') 108 | for v in ret: 109 | pid = int(v[1]) 110 | app_cpu_stat[pid] = pattern.findall(v[0]) 111 | stdout = stdout.replace(v[0], '') 112 | 113 | if not stdout: 114 | raise AdbNoInfoReturn(f'cpu信息获取异常') 115 | 116 | total_cpu_stat, core_cpu_stat = self._pares_cpu_stat(stdout) 117 | 118 | if not total_cpu_stat or not core_cpu_stat: 119 | return self._get_cpu_stat(name) 120 | 121 | return total_cpu_stat, core_cpu_stat, app_cpu_stat 122 | 123 | def _pares_cpu_stat(self, stat: str) -> Tuple[List[int], List[List[int]]]: 124 | """ 125 | 处理cpu信息数据 126 | 127 | Args: 128 | stat: cpu数据 129 | 130 | Returns: 131 | 总cpu数据和每个核心的数据 132 | """ 133 | total_cpu_stat = None 134 | core_cpu_stat = [] 135 | 136 | if total_stat := self.total_cpu_pattern.findall(stat): 137 | total_stat = self.cpu_jiffies_pattern.findall(total_stat[0]) 138 | total_cpu_stat = [int(v) for v in total_stat] 139 | 140 | if core_stat_list := self.core_stat_pattern.findall(stat): 141 | for core_stat in core_stat_list: 142 | _core_stat = self.cpu_jiffies_pattern.findall(core_stat[1].strip()) 143 | core_cpu_stat.append([int(v) for v in _core_stat]) 144 | 145 | if not total_cpu_stat or not core_cpu_stat: 146 | raise AdbNoInfoReturn('cpu信息获取异常') 147 | 148 | return total_cpu_stat, core_cpu_stat 149 | 150 | @staticmethod 151 | def _create_command(name: Optional[List[int]] = None): 152 | """ 153 | 根据pid创建cmd命令 154 | 155 | Args: 156 | name: 包含pid的列表 157 | 158 | Returns: 159 | cmd命令 160 | """ 161 | cmds = ['cat /proc/stat'] 162 | if name: 163 | for pid in name: 164 | cmds += [f'cat /proc/{pid}/stat'] 165 | 166 | return '&'.join(cmds) 167 | 168 | def _transform_name_to_pid(self, name: Union[List, Tuple]) -> Optional[List[int]]: 169 | """ 170 | 处理传参,将包名更改为pid 171 | 172 | Args: 173 | name: 需要处理的包名或pid 174 | 175 | Returns: 176 | 只包含pid的列表 177 | """ 178 | ret = [] 179 | for _name in name: 180 | if isinstance(_name, str): 181 | if pid := self.device.get_pid_by_name(_name): 182 | pid = pid[0][0] 183 | else: 184 | logger.warning(f"应用:'{_name}'未运行") 185 | pid = None 186 | elif isinstance(_name, int): 187 | pid = _name 188 | else: 189 | pid = None 190 | ret.append(pid) 191 | return ret 192 | 193 | 194 | if __name__ == '__main__': 195 | from adbutils import ADBDevice 196 | from adbutils.extra.performance.cpu import Cpu 197 | 198 | device_id = '' 199 | device = ADBDevice(device_id=device_id) 200 | top_watcher = Cpu(device) 201 | 202 | while True: 203 | total_cpu_usage, cpu_core_usage, app_usage_ret = top_watcher.get_cpu_usage(device.foreground_package) 204 | print('cpu={} core={}, {}'.format( 205 | f'{total_cpu_usage:.1f}%', 206 | '\t'.join([f'cpu{core_index}:{usage:.1f}%' for core_index, usage in enumerate(cpu_core_usage)]), 207 | '\t'.join([f'{name}:{usage:.1f}%' for name, usage in app_usage_ret.items()]) 208 | )) 209 | time.sleep(.5) 210 | -------------------------------------------------------------------------------- /adbutils/extra/performance/exceptions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from adbutils.exceptions import AdbBaseError 3 | 4 | 5 | class AdbProcessNotFound(AdbBaseError): 6 | """ An error while dumpsys meminfo process not found """ 7 | 8 | 9 | class AdbNoInfoReturn(AdbBaseError): 10 | """ An error while adb shell did not return the correct result """ 11 | -------------------------------------------------------------------------------- /adbutils/extra/performance/fps.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import re 3 | import time 4 | from typing import Optional, Union, List, Tuple 5 | 6 | from adbutils import ADBDevice 7 | 8 | from loguru import logger 9 | 10 | 11 | class Fps(object): 12 | _MIN_NORMALIZED_FRAME_LENGTH = 0.5 13 | Movie_FrameTime = 1000 / 24 / 1000 # 电影帧耗时,单位为秒 14 | nanoseconds_per_second = 1e9 # 纳秒转换成秒 15 | pending_fence_timestamp = (1 << 63) - 1 # 查询某帧数据出问题的时候,系统会返回一个64位的int最大值,忽略这列数据 16 | 17 | def __init__(self, device: ADBDevice): 18 | self.device = device 19 | self._last_drawEnd_timestamps = None 20 | 21 | def get_fps_surfaceView(self, surface_name: str): 22 | _Fps = 0 23 | _FTime = 0 24 | _Jank = 0 25 | _BigJank = 0 26 | _Stutter = 0 27 | 28 | # step1: 根据window名,获取帧数信息 29 | stat = self._get_surfaceFlinger_stat(surface_name) 30 | 31 | # step2: 提取帧数信息,分别得到刷新周期/绘制图像开始时间列表/绘制耗时列表/绘制结束列表 32 | refresh_period, _drawStart_timestamps, _vsync_timestamps, _drawEnd_timestamps = \ 33 | self._pares_surfaceFlinger_stat(stat) 34 | 35 | if not _drawStart_timestamps or not _vsync_timestamps or not _drawEnd_timestamps: 36 | return None 37 | 38 | # step3: 根据上次获取的最后一帧时间戳,切分本次有效帧列表 39 | if self._last_drawEnd_timestamps in _drawEnd_timestamps: 40 | index = list(reversed(_drawEnd_timestamps)).index(self._last_drawEnd_timestamps) 41 | index = len(_drawEnd_timestamps) - index + 1 42 | if index == len(_drawEnd_timestamps) - 1: 43 | index = 0 44 | else: 45 | index = 0 46 | 47 | drawStart_timestamps = _drawStart_timestamps[index:] 48 | vsync_timestamps = _vsync_timestamps[index:] 49 | drawEnd_timestamps = _drawEnd_timestamps[index:] 50 | 51 | # step4: 计算FPS(帧数) 52 | if (frame_count := len(vsync_timestamps)) < 2: 53 | return None 54 | seconds = vsync_timestamps[-1] - vsync_timestamps[0] 55 | _Fps = round((frame_count - 1) / seconds, 2) 56 | 57 | # step5: 计算FTime(帧耗时), 58 | vsync_frameTimes = self._get_frameTimes(vsync_timestamps) 59 | if vsync_frameTimes: 60 | _FTime = max(vsync_frameTimes) 61 | _FTime *= 1000 62 | 63 | # step6: 计算Jank 64 | # 由于step3只截取了有效帧,因此需要额外收集前三帧,用于计算帧耗时 65 | if index - 3 >= 0: 66 | jank_vsync_timestamps = _vsync_timestamps[index - 3:] 67 | else: 68 | jank_vsync_timestamps = vsync_timestamps 69 | jank_vsync_frameTimes = self._get_frameTimes(jank_vsync_timestamps) 70 | _Jank, _BigJank, Jank_time = self._get_perfdog_jank(jank_vsync_frameTimes) 71 | _Stutter = Jank_time / seconds * 100 72 | 73 | # step End: 记录最后一帧时间戳 74 | self._last_drawEnd_timestamps = drawEnd_timestamps[-1] 75 | 76 | return _Fps, _FTime, _Jank, _BigJank, _Stutter 77 | 78 | def clear_surfaceFlinger_latency(self) -> bool: 79 | """ 80 | command 'adb shell dumpsys SurfaceFlinger --latency-clear' 清除SurfaceFlinger latency里的数据 81 | 82 | Returns: 83 | True设备支持dumpsys SurfaceFlinger --latency, 否则为False 84 | """ 85 | ret = self.device.shell(['dumpsys', 'SurfaceFlinger', '--latency-clear']) 86 | return not len(ret) 87 | 88 | def _get_surfaceFlinger_stat(self, surface_name: Optional[str] = None) -> str: 89 | """ 90 | command 'adb shell dumpsys SurfaceFlinger --latency 91 | 92 | Returns: 93 | sufaceFlinger stat 94 | """ 95 | ret = self.device.shell(['dumpsys', 'SurfaceFlinger', '--latency', self._pares_activity_name(surface_name)]) 96 | if len(ret.splitlines()) > 1: 97 | return ret 98 | surface_name = self.get_possible_activity() or surface_name 99 | logger.warning('warning to get surfaceFlinger try again') 100 | return self._get_surfaceFlinger_stat(surface_name) 101 | 102 | def _pares_surfaceFlinger_stat(self, stat: str) -> Tuple[float, List[float], List[float], List[float]]: 103 | """ 104 | 处理SurfaceFlinger的信息,返回(刷新周期/绘制图像开始时间列表/绘制耗时列表/绘制结束列表) 105 | 106 | like: 107 | 16666666 108 | 10771456257842 10771499569140 10771456257842 109 | 10771473192729 10771516235806 10771473192729 110 | 10771490277889 10771532902472 10771490277889 111 | 10771507357378 10771549569138 10771507357378 112 | 10771523229435 10771566235804 10771523229435 113 | ... 114 | 115 | Args: 116 | stat: dumpsys SurfaceFlinger获得的信息 117 | 118 | Returns: 119 | 刷新周期/绘制图像开始时间列表/绘制耗时列表/绘制结束列表 120 | """ 121 | stat = stat.splitlines() 122 | # 三个列表分别对应dumpsys SurfaceFlinger的每一列 123 | 124 | # A) when the app started to draw 125 | # 开始绘制图像的瞬时时间 126 | drawStart_timestamps = [] 127 | 128 | # B) the vsync immediately preceding SF submitting the frame to the h/w 129 | # 垂直同步软件把帧提交给硬件之前的瞬时时间戳;VSYNC信令将软件SF帧传递给硬件HW之前的垂直同步时间 130 | vsync_timestamps = [] 131 | 132 | # C) timestamp immediately after SF submitted that frame to the h/w 133 | # 完成绘制的瞬时时间;SF将帧传递给HW的瞬时时间, 134 | drawEnd_timestamps = [] 135 | 136 | # 刷新周期 137 | refresh_period = int(stat[0]) / self.nanoseconds_per_second 138 | 139 | # 清除无用的空数据 140 | empty_data = [0, 0, 0] 141 | pattern = re.compile(r'(\d+)\s*(\d+)\s*(\d+)') 142 | for line in stat[1:]: 143 | # 确认数据结构,与数据是否有效 144 | fields = pattern.search(line) 145 | if not fields: 146 | continue 147 | 148 | # 判断数据是否有效 149 | fields = [int(v) for v in fields.groups()] 150 | if fields == empty_data or self.pending_fence_timestamp in fields: 151 | continue 152 | 153 | drawStart_timestamp = fields[0] 154 | drawStart_timestamp /= self.nanoseconds_per_second 155 | 156 | vsync_timestamp = fields[1] 157 | vsync_timestamp /= self.nanoseconds_per_second 158 | 159 | drawEnd_timestamp = fields[2] 160 | drawEnd_timestamp /= self.nanoseconds_per_second 161 | 162 | drawStart_timestamps.append(drawStart_timestamp) 163 | vsync_timestamps.append(vsync_timestamp) 164 | drawEnd_timestamps.append(drawEnd_timestamp) 165 | 166 | return refresh_period, drawStart_timestamps, vsync_timestamps, drawEnd_timestamps 167 | 168 | @staticmethod 169 | def _get_frameTimes(data: List[float]) -> List[float]: 170 | """ 171 | 计算两帧渲染耗时 172 | 173 | Args: 174 | data: 包含帧时间戳的列表 175 | 176 | Returns: 177 | 两帧渲染耗时列表 178 | """ 179 | deltas = [t2 - t1 for t1, t2 in zip(data, data[1:])] 180 | return deltas 181 | 182 | def _get_perfdog_jank(self, data: List[float]) -> Tuple[int, int, float]: 183 | """ 184 | 根据每帧耗时,计算jank 185 | 186 | 同时满足两条件,则认为是一次卡顿Jank. 187 | 1. FrameTime>前三帧平均耗时2倍。 188 | 2. FrameTime>两帧电影帧耗时 (1000ms/24*2≈83.33ms)。 189 | 190 | 同时满足两条件,则认为是一次严重卡顿BigJank. 191 | 1. FrameTime >前三帧平均耗时2倍。 192 | 2. FrameTime >三帧电影帧耗时(1000ms/24*3=125ms) 193 | Args: 194 | data: 每帧渲染耗时 195 | 196 | Returns: 197 | 198 | """ 199 | _jank = [new for new, *last in zip(data[3:], data[2:], data[1:], data) 200 | if (new >= self.Movie_FrameTime * 2) and (new > (sum(last) / 3) * 2)] 201 | 202 | _bigJank = [new for new, *last in zip(data[3:], data[2:], data[1:], data) 203 | if (new >= self.Movie_FrameTime * 3) and (new > (sum(last) / 3) * 2)] 204 | 205 | jank = len(_jank) 206 | bigJank = len(_bigJank) 207 | jank_time = sum(_jank) + sum(_bigJank) 208 | 209 | return jank, bigJank, jank_time 210 | 211 | def get_possible_activity(self) -> Optional[str]: 212 | """ 213 | 通过 ‘dumpsys SurfaceFlinger --list',查找到当前最顶部层级名 214 | 215 | Returns: 216 | 包含可能层级 217 | """ 218 | ret = self.device.shell(['dumpsys', 'SurfaceFlinger', '--list']).strip().splitlines() 219 | # 特殊适配谷歌手机 220 | if self.device.manufacturer == 'Google': 221 | # adb shell dumpsys SurfaceFlinger --latency 'SurfaceView[xx.xx.xx.xx/org.xx.lua.AppActivity](BLAST)#0' 222 | buffering_stats_pattern = re.compile(r'SurfaceView\[.*\(BLAST\).*', re.DOTALL) 223 | else: 224 | buffering_stats_pattern = re.compile(r'SurfaceView -.*', re.DOTALL) 225 | for layer in ret: 226 | if layers := buffering_stats_pattern.search(layer): 227 | return layers.group() 228 | logger.error("Don't find SurfaceView") 229 | 230 | def check_activity_usable(self, activity_name: str) -> Optional[str]: 231 | """ 232 | 检查activity是否有效,command 'adb shell dumpsys SurfaceFlinge --latency ' 233 | 如果只返回了刷新周期,则认为该activity无效 234 | 235 | Args: 236 | activity_name: 需要检查的activity名 237 | 238 | Returns: 239 | 满足条件的activity 240 | """ 241 | if stat := self._get_surfaceFlinger_stat(activity_name): 242 | stat_len = len(stat.splitlines()) 243 | if stat_len > 1: 244 | return activity_name 245 | 246 | @staticmethod 247 | def _pares_activity_name(activity_name: str = None) -> Optional[str]: 248 | """ 249 | 检查activity名是否符合标准 250 | 251 | Args: 252 | activity_name: activity名 253 | 254 | Returns: 255 | 处理后的activity名 256 | """ 257 | if not activity_name or not isinstance(activity_name, str): 258 | return '' 259 | 260 | # 检查是否使用冒号包裹 261 | pattern = re.compile(r"^'(.*)'$") 262 | if pattern.search(activity_name): 263 | return activity_name 264 | # 包含空格和小括号都要被冒号包裹 265 | pattern = re.compile(r'\s|\(') 266 | if pattern.search(activity_name): 267 | activity_name = f"'{activity_name}'" 268 | return activity_name 269 | -------------------------------------------------------------------------------- /adbutils/extra/performance/meminfo.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import re 3 | from typing import Match, Optional, Union, Dict, Tuple, List 4 | 5 | from adbutils import ADBDevice 6 | from .exceptions import AdbProcessNotFound 7 | 8 | 9 | class Meminfo(object): 10 | def __init__(self, device: ADBDevice): 11 | """ 12 | 获取设备性能数据 13 | 14 | Args: 15 | device: adb设备类 16 | """ 17 | self.device = device 18 | 19 | def get_pss_by_process(self, meminfo: Optional[str] = None) -> List[Tuple[int, str, int]]: 20 | """ 21 | 获取系统所有进程的pss内存大小,单位(KB)\n 22 | 返回列表,每个参数都是tuple(memory,package_name,pid) 23 | 24 | Returns: 25 | 所有进程的pss内存 26 | """ 27 | meminfo = meminfo or self.get_system_meminfo() 28 | if m := self._parse_system_meminfo(meminfo): 29 | ret = [] 30 | pss_by_process = m.group('pss_by_process').strip() 31 | pss_by_process = pss_by_process.splitlines() 32 | pattern = re.compile(r'(?P\S+K): (?P\S+)\s*\(pid (?P\d+)') 33 | for process in pss_by_process: 34 | m = pattern.search(process.strip()) 35 | memory, name, pid = self._pares_memory(m.group('memory')), m.group('name'), int(m.group('pid')) 36 | ret.append((memory, name, pid)) 37 | return ret 38 | 39 | def get_total_ram(self, meminfo: Optional[str] = None) -> int: 40 | """ 41 | 获取全部内存(total_ram)大小,单位(KB) 42 | 43 | Args: 44 | meminfo: 内存信息 45 | 46 | Returns: 47 | 内存大小(KB) 48 | """ 49 | meminfo = meminfo or self.get_system_meminfo() 50 | if m := self._parse_system_meminfo(meminfo): 51 | return self._pares_memory(m.group('total_ram')) 52 | 53 | def get_free_ram(self, meminfo: Optional[str] = None) -> int: 54 | """ 55 | 获取Free RAM大小,单位(KB) 56 | 57 | Args: 58 | meminfo: 内存信息 59 | 60 | Returns: 61 | Free RAM(KB) 62 | """ 63 | meminfo = meminfo or self.get_system_meminfo() 64 | if m := self._parse_system_meminfo(meminfo): 65 | return self._pares_memory(m.group('free_ram')) 66 | 67 | def get_used_ram(self, meminfo: Optional[str] = None) -> int: 68 | """ 69 | 获取Used RAM大小,单位(KB) 70 | 71 | Args: 72 | meminfo: 内存信息 73 | 74 | Returns: 75 | Used RAM(KB) 76 | """ 77 | meminfo = meminfo or self.get_system_meminfo() 78 | if m := self._parse_system_meminfo(meminfo): 79 | return self._pares_memory(m.group('used_ram')) 80 | 81 | def get_lost_ram(self, meminfo: Optional[str] = None) -> int: 82 | """ 83 | 获取Lost RAM大小,单位(KB) 84 | 85 | Args: 86 | meminfo: 内存信息 87 | 88 | Returns: 89 | Lost RAM(KB) 90 | """ 91 | meminfo = meminfo or self.get_system_meminfo() 92 | if m := self._parse_system_meminfo(meminfo): 93 | return self._pares_memory(m.group('lost_ram')) 94 | 95 | def get_app_meminfo(self, package: Union[str, int]) -> Optional[Dict[str, Dict[str, int]]]: 96 | """ 97 | 获取指定包或进程号的内存信息 98 | 键为内存条目名称 99 | 值是一个包含pss_total/private_dirty/private_clean/swapPss_dirty/heap_size/heap_alloc/heap_free的字典,对应的内存占用(KB) 100 | 101 | Args: 102 | package: 包名或pid进程号 103 | 104 | Returns: 105 | 以字典返回所有内存信息 106 | """ 107 | if not package: 108 | return None 109 | ret = {} 110 | _memory_info_count = 7 111 | if meminfo := self._get_app_meminfo(package): 112 | meminfo = self._parse_app_meminfo(meminfo) 113 | meminfo = meminfo.group('meminfo').strip() 114 | meminfo = meminfo.splitlines() 115 | # get meminfo head 116 | head_pattern = re.compile(r'(\S+)') 117 | headLine = [f'{t_1.lower()}_{t_2.lower()}' for t_1, t_2 in 118 | zip(head_pattern.findall(meminfo[0].strip()), head_pattern.findall(meminfo[1].strip()))] 119 | 120 | pattern = re.compile(r'\s*(\S+\s?\S*)\s*(.*)\r?') 121 | for line in meminfo[3:]: 122 | line = pattern.search(line) 123 | if not line: 124 | continue 125 | 126 | mem_pattern = re.compile(r'(\d+)\s*') 127 | mem = mem_pattern.findall(line.group(2).strip()) 128 | # 将无内存信息的,补全0 129 | mem += [0 for _ in range(_memory_info_count - len(mem))] 130 | 131 | name = line.group(1).strip().lower().replace(' ', '_') 132 | 133 | ret[name] = {headLine[index]: v for index, v in enumerate([int(v) for v in mem])} 134 | return ret 135 | 136 | def get_app_summary(self, package: Union[str, int]) -> Optional[Dict[str, int]]: 137 | """ 138 | 获取app summary pss。 139 | 返回一个包含java_heap/native_heap/code/stack/graphics/private_other/system/total/total_swap_pss的字典 140 | 141 | Args: 142 | package: 包名或pid进程号 143 | 144 | Returns: 145 | app内存信息概要 146 | """ 147 | if not package: 148 | return None 149 | ret = {} 150 | if meminfo := self._get_app_meminfo(package): 151 | meminfo = self._parse_app_meminfo(meminfo) 152 | meminfo = meminfo.group('app_summary').strip() 153 | pattern = re.compile(r'\s*(\S+\s?\S*\s?\S*):\s*(\d+)') 154 | for v in pattern.findall(meminfo): 155 | name = v[0].strip().lower().replace(' ', '_') 156 | memory = int(v[1]) 157 | ret[name] = memory 158 | return ret 159 | 160 | @staticmethod 161 | def _pares_memory(memory: str): 162 | """ 163 | 处理字符串memory,转换为int,单位KB 164 | 165 | Args: 166 | memory(str): 内存 167 | 168 | Returns: 169 | 内存大小,单位(KB) 170 | """ 171 | memory = int(memory.strip().replace(',', '').replace('K', '')) 172 | return memory 173 | 174 | @staticmethod 175 | def _parse_system_meminfo(meminfo: str) -> Optional[Match[str]]: 176 | """ 177 | 处理adb shell dumpsys meminfo返回的数据 178 | 179 | Args: 180 | meminfo: 内存数据 181 | 182 | Returns: 183 | 包含uptime、realtime、pss_by_process、pss_by_ommAdjustemnt、pss_by_category、 184 | total_ram、free_ram、used_ram、lost_ram 185 | """ 186 | pattern = re.compile(r'Uptime: (?P\d+) Realtime: (?P\d+)' 187 | r'.*Total PSS by process:(?P.*)' 188 | r'.*Total PSS by OOM adjustment:(?P.*)' 189 | r'.*Total PSS by category:(?P.*)' 190 | r'.*Total RAM:\s*(?P\S+)' 191 | r'.*Free RAM:\s*(?P\S+)' 192 | r'.*Used RAM:\s*(?P\S+)' 193 | r'.*Lost RAM:\s*(?P\S+)', re.DOTALL) 194 | return m if (m := pattern.search(meminfo)) else None 195 | 196 | @staticmethod 197 | def _parse_app_meminfo(meminfo: str): 198 | pattern = re.compile(r'Uptime: (?P\d+) Realtime: (?P\d+)' 199 | r'.*\*\* MEMINFO in pid (?P\d+) \[(?P\S+)] \*\*' 200 | r'(?P.*)' 201 | r'.*App Summary\s*(?P.*)' 202 | r'.*Objects(.*)', re.DOTALL) 203 | return m if (m := pattern.search(meminfo)) else None 204 | 205 | def _get_app_meminfo(self, packageName: Union[str, int]): 206 | """ 207 | 'adb shell dumpsys meminfo ' 获取指定包或进程号的内存信息 208 | 209 | Raises: 210 | AdbProcessNotFound: 未找到对应进程时弹出异常 211 | Args: 212 | packageName: 包名或pid进程号 213 | 214 | Returns: 215 | 内存信息 216 | """ 217 | if isinstance(packageName, int): 218 | arg = packageName 219 | else: 220 | arg = self.device.get_pid_by_name(packageName=packageName) 221 | if not arg: 222 | raise AdbProcessNotFound(f"'{packageName}' not found") 223 | arg = str(arg[0][0]) 224 | 225 | if 'No process found for:' in (ret := self.device.shell(['dumpsys', 'meminfo', arg])): 226 | raise AdbProcessNotFound(ret.strip()) 227 | else: 228 | return ret 229 | 230 | def get_system_meminfo(self) -> str: 231 | """ 232 | 'adb shell dumpsys meminfo' 获取系统内存信息 233 | 234 | Returns: 235 | 内存信息 236 | """ 237 | return self.device.shell(['dumpsys', 'meminfo']) 238 | 239 | 240 | if __name__ == '__main__': 241 | from adbutils import ADBDevice 242 | from adbutils.extra.performance.meminfo import Meminfo 243 | 244 | device = ADBDevice(device_id='') 245 | performance = Meminfo(device) 246 | 247 | performance.get_system_meminfo() 248 | 249 | for i in range(100): 250 | package_name = device.foreground_package 251 | total = performance.get_app_summary(package_name)['total'] 252 | # 打印package_name对应app的内存占用 253 | print(f'{(int(total) / 1024):.0f}MB') 254 | -------------------------------------------------------------------------------- /adbutils/extra/rotation/__init__.py: -------------------------------------------------------------------------------- 1 | #! usr/bin/python 2 | # -*- coding:utf-8 -*- 3 | import os 4 | import time 5 | import threading 6 | import traceback 7 | from loguru import logger 8 | 9 | 10 | class Rotation(object): 11 | def __init__(self, device): 12 | self.device = device 13 | self._kill_event = threading.Event() 14 | self.current_orientation = None 15 | self._t = None 16 | self.ow_callback = [] 17 | 18 | def start(self): 19 | 20 | def _refresh_by_adb(): 21 | ori = self.device.getDisplayOrientation() 22 | return ori 23 | 24 | def _run(kill_event): 25 | while not kill_event.is_set(): 26 | ori = _refresh_by_adb() 27 | if self.current_orientation == ori: 28 | time.sleep(2) 29 | continue 30 | if ori is None: 31 | continue 32 | logger.info('update orientation {}->{}'.format(self.current_orientation, ori)) 33 | self.current_orientation = ori 34 | for callback in self.ow_callback: 35 | try: 36 | callback(ori) 37 | except Exception: 38 | logger.error('callback: {} error'.format(callback)) 39 | traceback.print_exc() 40 | 41 | self.current_orientation = _refresh_by_adb() 42 | self._t = threading.Thread(target=_run, args=(self._kill_event,), name='rotationwatcher') 43 | self._t.daemon = True 44 | self._t.start() 45 | return self.current_orientation 46 | 47 | def reg_callback(self, ow_callback): 48 | """ 49 | Args: 50 | ow_callback: 51 | Returns: 52 | """ 53 | """方向变化的时候的回调函数,参数一定是ori""" 54 | self.ow_callback.append(ow_callback) 55 | -------------------------------------------------------------------------------- /adbutils/static/ADBKeyboard.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/ADBKeyboard.apk -------------------------------------------------------------------------------- /adbutils/static/aapt2/arm64-v8a/bin/aapt2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/aapt2/arm64-v8a/bin/aapt2 -------------------------------------------------------------------------------- /adbutils/static/aapt2/arm64-v8a/lib/libaapt2.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/aapt2/arm64-v8a/lib/libaapt2.so -------------------------------------------------------------------------------- /adbutils/static/aapt2/arm64-v8a/lib64/libaapt2.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/aapt2/arm64-v8a/lib64/libaapt2.so -------------------------------------------------------------------------------- /adbutils/static/aapt2/armeabi-v7a/bin/aapt2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/aapt2/armeabi-v7a/bin/aapt2 -------------------------------------------------------------------------------- /adbutils/static/aapt2/armeabi-v7a/lib/libaapt2.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/aapt2/armeabi-v7a/lib/libaapt2.so -------------------------------------------------------------------------------- /adbutils/static/aapt2/x86/bin/aapt2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/aapt2/x86/bin/aapt2 -------------------------------------------------------------------------------- /adbutils/static/aapt2/x86/lib/libaapt2.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/aapt2/x86/lib/libaapt2.so -------------------------------------------------------------------------------- /adbutils/static/aapt2/x86_64/bin/aapt2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/aapt2/x86_64/bin/aapt2 -------------------------------------------------------------------------------- /adbutils/static/aapt2/x86_64/lib/libaapt2.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/aapt2/x86_64/lib/libaapt2.so -------------------------------------------------------------------------------- /adbutils/static/aapt2/x86_64/lib64/libaapt2.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/aapt2/x86_64/lib64/libaapt2.so -------------------------------------------------------------------------------- /adbutils/static/adb/linux/adb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/adb/linux/adb -------------------------------------------------------------------------------- /adbutils/static/adb/linux_arm/adb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/adb/linux_arm/adb -------------------------------------------------------------------------------- /adbutils/static/adb/mac/adb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/adb/mac/adb -------------------------------------------------------------------------------- /adbutils/static/adb/windows/AdbWinApi.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/adb/windows/AdbWinApi.dll -------------------------------------------------------------------------------- /adbutils/static/adb/windows/AdbWinUsbApi.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/adb/windows/AdbWinUsbApi.dll -------------------------------------------------------------------------------- /adbutils/static/adb/windows/adb.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/adb/windows/adb.exe -------------------------------------------------------------------------------- /adbutils/static/busyBox/busybox-armv5l: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/busyBox/busybox-armv5l -------------------------------------------------------------------------------- /adbutils/static/busyBox/busybox-armv7l: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/busyBox/busybox-armv7l -------------------------------------------------------------------------------- /adbutils/static/busyBox/busybox-armv7m: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/busyBox/busybox-armv7m -------------------------------------------------------------------------------- /adbutils/static/busyBox/busybox-armv7r: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/busyBox/busybox-armv7r -------------------------------------------------------------------------------- /adbutils/static/busyBox/busybox-armv8l: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/busyBox/busybox-armv8l -------------------------------------------------------------------------------- /adbutils/static/stf_libs/arm64-v8a/minicap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/arm64-v8a/minicap -------------------------------------------------------------------------------- /adbutils/static/stf_libs/arm64-v8a/minicap-nopie: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/arm64-v8a/minicap-nopie -------------------------------------------------------------------------------- /adbutils/static/stf_libs/arm64-v8a/minitouch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/arm64-v8a/minitouch -------------------------------------------------------------------------------- /adbutils/static/stf_libs/arm64-v8a/minitouch-nopie: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/arm64-v8a/minitouch-nopie -------------------------------------------------------------------------------- /adbutils/static/stf_libs/armeabi-v7a/minicap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/armeabi-v7a/minicap -------------------------------------------------------------------------------- /adbutils/static/stf_libs/armeabi-v7a/minicap-nopie: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/armeabi-v7a/minicap-nopie -------------------------------------------------------------------------------- /adbutils/static/stf_libs/armeabi-v7a/minitouch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/armeabi-v7a/minitouch -------------------------------------------------------------------------------- /adbutils/static/stf_libs/armeabi-v7a/minitouch-nopie: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/armeabi-v7a/minitouch-nopie -------------------------------------------------------------------------------- /adbutils/static/stf_libs/armeabi/minitouch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/armeabi/minitouch -------------------------------------------------------------------------------- /adbutils/static/stf_libs/armeabi/minitouch-nopie: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/armeabi/minitouch-nopie -------------------------------------------------------------------------------- /adbutils/static/stf_libs/minicap-shared/aosp/libs/android-10/armeabi-v7a/minicap.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/minicap-shared/aosp/libs/android-10/armeabi-v7a/minicap.so -------------------------------------------------------------------------------- /adbutils/static/stf_libs/minicap-shared/aosp/libs/android-14/armeabi-v7a/minicap.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/minicap-shared/aosp/libs/android-14/armeabi-v7a/minicap.so -------------------------------------------------------------------------------- /adbutils/static/stf_libs/minicap-shared/aosp/libs/android-14/x86/minicap.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/minicap-shared/aosp/libs/android-14/x86/minicap.so -------------------------------------------------------------------------------- /adbutils/static/stf_libs/minicap-shared/aosp/libs/android-15/armeabi-v7a/minicap.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/minicap-shared/aosp/libs/android-15/armeabi-v7a/minicap.so -------------------------------------------------------------------------------- /adbutils/static/stf_libs/minicap-shared/aosp/libs/android-15/x86/minicap.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/minicap-shared/aosp/libs/android-15/x86/minicap.so -------------------------------------------------------------------------------- /adbutils/static/stf_libs/minicap-shared/aosp/libs/android-16/armeabi-v7a/minicap.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/minicap-shared/aosp/libs/android-16/armeabi-v7a/minicap.so -------------------------------------------------------------------------------- /adbutils/static/stf_libs/minicap-shared/aosp/libs/android-16/x86/minicap.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/minicap-shared/aosp/libs/android-16/x86/minicap.so -------------------------------------------------------------------------------- /adbutils/static/stf_libs/minicap-shared/aosp/libs/android-17/armeabi-v7a/minicap.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/minicap-shared/aosp/libs/android-17/armeabi-v7a/minicap.so -------------------------------------------------------------------------------- /adbutils/static/stf_libs/minicap-shared/aosp/libs/android-17/x86/minicap.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/minicap-shared/aosp/libs/android-17/x86/minicap.so -------------------------------------------------------------------------------- /adbutils/static/stf_libs/minicap-shared/aosp/libs/android-18/armeabi-v7a/minicap.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/minicap-shared/aosp/libs/android-18/armeabi-v7a/minicap.so -------------------------------------------------------------------------------- /adbutils/static/stf_libs/minicap-shared/aosp/libs/android-18/x86/minicap.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/minicap-shared/aosp/libs/android-18/x86/minicap.so -------------------------------------------------------------------------------- /adbutils/static/stf_libs/minicap-shared/aosp/libs/android-19/armeabi-v7a/minicap.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/minicap-shared/aosp/libs/android-19/armeabi-v7a/minicap.so -------------------------------------------------------------------------------- /adbutils/static/stf_libs/minicap-shared/aosp/libs/android-19/x86/minicap.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/minicap-shared/aosp/libs/android-19/x86/minicap.so -------------------------------------------------------------------------------- /adbutils/static/stf_libs/minicap-shared/aosp/libs/android-21/arm64-v8a/minicap.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/minicap-shared/aosp/libs/android-21/arm64-v8a/minicap.so -------------------------------------------------------------------------------- /adbutils/static/stf_libs/minicap-shared/aosp/libs/android-21/armeabi-v7a/minicap.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/minicap-shared/aosp/libs/android-21/armeabi-v7a/minicap.so -------------------------------------------------------------------------------- /adbutils/static/stf_libs/minicap-shared/aosp/libs/android-21/x86/minicap.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/minicap-shared/aosp/libs/android-21/x86/minicap.so -------------------------------------------------------------------------------- /adbutils/static/stf_libs/minicap-shared/aosp/libs/android-21/x86_64/minicap.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/minicap-shared/aosp/libs/android-21/x86_64/minicap.so -------------------------------------------------------------------------------- /adbutils/static/stf_libs/minicap-shared/aosp/libs/android-22/arm64-v8a/minicap.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/minicap-shared/aosp/libs/android-22/arm64-v8a/minicap.so -------------------------------------------------------------------------------- /adbutils/static/stf_libs/minicap-shared/aosp/libs/android-22/armeabi-v7a/minicap.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/minicap-shared/aosp/libs/android-22/armeabi-v7a/minicap.so -------------------------------------------------------------------------------- /adbutils/static/stf_libs/minicap-shared/aosp/libs/android-22/x86/minicap.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/minicap-shared/aosp/libs/android-22/x86/minicap.so -------------------------------------------------------------------------------- /adbutils/static/stf_libs/minicap-shared/aosp/libs/android-22/x86_64/minicap.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/minicap-shared/aosp/libs/android-22/x86_64/minicap.so -------------------------------------------------------------------------------- /adbutils/static/stf_libs/minicap-shared/aosp/libs/android-23/arm64-v8a/minicap.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/minicap-shared/aosp/libs/android-23/arm64-v8a/minicap.so -------------------------------------------------------------------------------- /adbutils/static/stf_libs/minicap-shared/aosp/libs/android-23/armeabi-v7a/minicap.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/minicap-shared/aosp/libs/android-23/armeabi-v7a/minicap.so -------------------------------------------------------------------------------- /adbutils/static/stf_libs/minicap-shared/aosp/libs/android-23/x86/minicap.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/minicap-shared/aosp/libs/android-23/x86/minicap.so -------------------------------------------------------------------------------- /adbutils/static/stf_libs/minicap-shared/aosp/libs/android-23/x86_64/minicap.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/minicap-shared/aosp/libs/android-23/x86_64/minicap.so -------------------------------------------------------------------------------- /adbutils/static/stf_libs/minicap-shared/aosp/libs/android-24/arm64-v8a/minicap.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/minicap-shared/aosp/libs/android-24/arm64-v8a/minicap.so -------------------------------------------------------------------------------- /adbutils/static/stf_libs/minicap-shared/aosp/libs/android-24/armeabi-v7a/minicap.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/minicap-shared/aosp/libs/android-24/armeabi-v7a/minicap.so -------------------------------------------------------------------------------- /adbutils/static/stf_libs/minicap-shared/aosp/libs/android-24/x86/minicap.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/minicap-shared/aosp/libs/android-24/x86/minicap.so -------------------------------------------------------------------------------- /adbutils/static/stf_libs/minicap-shared/aosp/libs/android-24/x86_64/minicap.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/minicap-shared/aosp/libs/android-24/x86_64/minicap.so -------------------------------------------------------------------------------- /adbutils/static/stf_libs/minicap-shared/aosp/libs/android-25/arm64-v8a/minicap.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/minicap-shared/aosp/libs/android-25/arm64-v8a/minicap.so -------------------------------------------------------------------------------- /adbutils/static/stf_libs/minicap-shared/aosp/libs/android-25/armeabi-v7a/minicap.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/minicap-shared/aosp/libs/android-25/armeabi-v7a/minicap.so -------------------------------------------------------------------------------- /adbutils/static/stf_libs/minicap-shared/aosp/libs/android-25/x86/minicap.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/minicap-shared/aosp/libs/android-25/x86/minicap.so -------------------------------------------------------------------------------- /adbutils/static/stf_libs/minicap-shared/aosp/libs/android-25/x86_64/minicap.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/minicap-shared/aosp/libs/android-25/x86_64/minicap.so -------------------------------------------------------------------------------- /adbutils/static/stf_libs/minicap-shared/aosp/libs/android-26/arm64-v8a/minicap.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/minicap-shared/aosp/libs/android-26/arm64-v8a/minicap.so -------------------------------------------------------------------------------- /adbutils/static/stf_libs/minicap-shared/aosp/libs/android-26/armeabi-v7a/minicap.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/minicap-shared/aosp/libs/android-26/armeabi-v7a/minicap.so -------------------------------------------------------------------------------- /adbutils/static/stf_libs/minicap-shared/aosp/libs/android-26/x86/minicap.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/minicap-shared/aosp/libs/android-26/x86/minicap.so -------------------------------------------------------------------------------- /adbutils/static/stf_libs/minicap-shared/aosp/libs/android-26/x86_64/minicap.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/minicap-shared/aosp/libs/android-26/x86_64/minicap.so -------------------------------------------------------------------------------- /adbutils/static/stf_libs/minicap-shared/aosp/libs/android-27/arm64-v8a/minicap.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/minicap-shared/aosp/libs/android-27/arm64-v8a/minicap.so -------------------------------------------------------------------------------- /adbutils/static/stf_libs/minicap-shared/aosp/libs/android-27/armeabi-v7a/minicap.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/minicap-shared/aosp/libs/android-27/armeabi-v7a/minicap.so -------------------------------------------------------------------------------- /adbutils/static/stf_libs/minicap-shared/aosp/libs/android-27/x86/minicap.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/minicap-shared/aosp/libs/android-27/x86/minicap.so -------------------------------------------------------------------------------- /adbutils/static/stf_libs/minicap-shared/aosp/libs/android-27/x86_64/minicap.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/minicap-shared/aosp/libs/android-27/x86_64/minicap.so -------------------------------------------------------------------------------- /adbutils/static/stf_libs/minicap-shared/aosp/libs/android-28/arm64-v8a/minicap.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/minicap-shared/aosp/libs/android-28/arm64-v8a/minicap.so -------------------------------------------------------------------------------- /adbutils/static/stf_libs/minicap-shared/aosp/libs/android-28/armeabi-v7a/minicap.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/minicap-shared/aosp/libs/android-28/armeabi-v7a/minicap.so -------------------------------------------------------------------------------- /adbutils/static/stf_libs/minicap-shared/aosp/libs/android-28/x86/minicap.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/minicap-shared/aosp/libs/android-28/x86/minicap.so -------------------------------------------------------------------------------- /adbutils/static/stf_libs/minicap-shared/aosp/libs/android-28/x86_64/minicap.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/minicap-shared/aosp/libs/android-28/x86_64/minicap.so -------------------------------------------------------------------------------- /adbutils/static/stf_libs/minicap-shared/aosp/libs/android-29/arm64-v8a/minicap.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/minicap-shared/aosp/libs/android-29/arm64-v8a/minicap.so -------------------------------------------------------------------------------- /adbutils/static/stf_libs/minicap-shared/aosp/libs/android-29/armeabi-v7a/minicap.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/minicap-shared/aosp/libs/android-29/armeabi-v7a/minicap.so -------------------------------------------------------------------------------- /adbutils/static/stf_libs/minicap-shared/aosp/libs/android-29/x86/minicap.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/minicap-shared/aosp/libs/android-29/x86/minicap.so -------------------------------------------------------------------------------- /adbutils/static/stf_libs/minicap-shared/aosp/libs/android-29/x86_64/minicap.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/minicap-shared/aosp/libs/android-29/x86_64/minicap.so -------------------------------------------------------------------------------- /adbutils/static/stf_libs/minicap-shared/aosp/libs/android-30/arm64-v8a/minicap.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/minicap-shared/aosp/libs/android-30/arm64-v8a/minicap.so -------------------------------------------------------------------------------- /adbutils/static/stf_libs/minicap-shared/aosp/libs/android-30/armeabi-v7a/minicap.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/minicap-shared/aosp/libs/android-30/armeabi-v7a/minicap.so -------------------------------------------------------------------------------- /adbutils/static/stf_libs/minicap-shared/aosp/libs/android-30/x86/minicap.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/minicap-shared/aosp/libs/android-30/x86/minicap.so -------------------------------------------------------------------------------- /adbutils/static/stf_libs/minicap-shared/aosp/libs/android-30/x86_64/minicap.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/minicap-shared/aosp/libs/android-30/x86_64/minicap.so -------------------------------------------------------------------------------- /adbutils/static/stf_libs/minicap-shared/aosp/libs/android-9/armeabi-v7a/minicap.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/minicap-shared/aosp/libs/android-9/armeabi-v7a/minicap.so -------------------------------------------------------------------------------- /adbutils/static/stf_libs/mips/minitouch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/mips/minitouch -------------------------------------------------------------------------------- /adbutils/static/stf_libs/mips/minitouch-nopie: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/mips/minitouch-nopie -------------------------------------------------------------------------------- /adbutils/static/stf_libs/mips64/minitouch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/mips64/minitouch -------------------------------------------------------------------------------- /adbutils/static/stf_libs/mips64/minitouch-nopie: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/mips64/minitouch-nopie -------------------------------------------------------------------------------- /adbutils/static/stf_libs/x86/minicap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/x86/minicap -------------------------------------------------------------------------------- /adbutils/static/stf_libs/x86/minicap-nopie: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/x86/minicap-nopie -------------------------------------------------------------------------------- /adbutils/static/stf_libs/x86/minitouch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/x86/minitouch -------------------------------------------------------------------------------- /adbutils/static/stf_libs/x86/minitouch-nopie: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/x86/minitouch-nopie -------------------------------------------------------------------------------- /adbutils/static/stf_libs/x86_64/minicap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/x86_64/minicap -------------------------------------------------------------------------------- /adbutils/static/stf_libs/x86_64/minicap-nopie: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/x86_64/minicap-nopie -------------------------------------------------------------------------------- /adbutils/static/stf_libs/x86_64/minitouch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/x86_64/minitouch -------------------------------------------------------------------------------- /adbutils/static/stf_libs/x86_64/minitouch-nopie: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hakaboom/adb-utils/18de69848f6a3920de1406925ce618ee501277c5/adbutils/static/stf_libs/x86_64/minitouch-nopie -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | """ 2 | python setup.py sdist 3 | twine upload dist/* 4 | """ 5 | import re 6 | import sys 7 | import time 8 | import os 9 | from loguru import logger 10 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | baseImage==2.1.2 2 | loguru>=0.5.3 3 | pydantic==1.8.2 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from setuptools import setup, find_packages 3 | 4 | with open("README.md", "r", encoding="utf-8") as fh: 5 | long_description = fh.read() 6 | 7 | 8 | setup( 9 | name='adb-utils', 10 | version='1.0.17', 11 | author='hakaboom', 12 | author_email='1534225986@qq.com', 13 | license='Apache License 2.0', 14 | description='This is a secondary package of adb', 15 | long_description=long_description, 16 | long_description_content_type="text/markdown", 17 | url='https://github.com/hakaboom/adb-utils', 18 | packages=find_packages(), 19 | include_package_data=True, 20 | install_requires=["loguru>=0.5.3", 21 | "baseImage==1.1.1"], 22 | classifiers=[ 23 | "Programming Language :: Python :: 3", 24 | "License :: OSI Approved :: Apache Software License", 25 | "Operating System :: OS Independent", 26 | ], 27 | python_requires='>=3.8', 28 | ) 29 | --------------------------------------------------------------------------------