├── .bumpversion.cfg ├── .gitattributes ├── .gitignore ├── .travis.yml ├── Dockerfile ├── HISTORY.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── bumpversion.sh ├── docker-compose.yaml ├── docs ├── adb_cli.png └── adb_pure_python_adb.png ├── example └── screencap.py ├── ppadb ├── __init__.py ├── application.py ├── client.py ├── client_async.py ├── command │ ├── __init__.py │ ├── host │ │ └── __init__.py │ ├── host_async │ │ └── __init__.py │ ├── serial │ │ └── __init__.py │ ├── transport │ │ └── __init__.py │ └── transport_async │ │ └── __init__.py ├── connection.py ├── connection_async.py ├── device.py ├── device_async.py ├── keycode.py ├── plugins │ ├── __init__.py │ ├── client │ │ └── __init__.py │ └── device │ │ ├── __init__.py │ │ ├── batterystats.py │ │ ├── batterystats_section.py │ │ ├── cpustat.py │ │ ├── input.py │ │ ├── traffic.py │ │ ├── utils.py │ │ └── wm.py ├── protocol.py ├── sync │ ├── __init__.py │ └── stats.py ├── sync_async │ └── __init__.py └── utils │ ├── __init__.py │ └── logger.py ├── requirements.txt ├── scripts └── batterystats_codegen.py ├── setup.py ├── test ├── __init__.py ├── conftest.py ├── resources │ └── apk │ │ ├── app-armeabi-v7a.apk │ │ └── app-x86.apk ├── test_batterystats.py ├── test_cpu_stat.py ├── test_device.py ├── test_host.py ├── test_host_serial.py ├── test_logging.py ├── test_plugins.py └── test_transport.py └── test_async ├── __init__.py ├── async_wrapper.py ├── patchers.py ├── test_client_async.py ├── test_connection_async.py └── test_device_async.py /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | commit = True 3 | tag = True 4 | current_version = 0.3.0-dev 5 | parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\-(?P[a-z]+))? 6 | serialize = 7 | {major}.{minor}.{patch}-{release} 8 | {major}.{minor}.{patch} 9 | 10 | [bumpversion:file:setup.py] 11 | 12 | [bumpversion:file:ppadb/__init__.py] 13 | 14 | [bumpversion:part:release] 15 | optional_value = release 16 | values = 17 | dev 18 | release 19 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | adb/_version.py export-subst 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | __pycache__ 3 | *.png 4 | .cache 5 | dist 6 | *.egg-info 7 | .repo 8 | .pytest_cache 9 | *.pyc 10 | venv 11 | test_result.xml 12 | .python-version -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: python 3 | python: 4 | - '3.6' 5 | install: 6 | - pip install -r requirements.txt 7 | script: 8 | - echo "skip test" 9 | deploy: 10 | skip_cleanup: true 11 | provider: pypi 12 | user: swind 13 | password: 14 | secure: GjGXSJk5e52is++wzUv0/SmWtEAIXv7WcFq+p+SzFx4aOn+gnT6yp0zY/9C4qNFJk8p2TbCawbFicK7WUCqd0QoUTiG495SwlIEf5FHa2L0qWFVU2KLNLXB/FD2GosqnpsL/FIOwIqFzZ/PjeEpnDx45d7yZhXR571bdgMxzah89gQZgsHS2eBrjwKMNjTRpNP+hlGbiMRLyYh2Ay0UJG4wGL92FoOooboB6z473ZdyIKq5Hsah4+WhvQWV/oPwqLS18us4tF0zd4YO4cG8T+cgdIBF/+xgpQTSQEaWWAzi/f1PK99KtNJuy2NV+TFL03CMRBSfr0UDMoU4neSKVNXC51F9uNQCTQN2NGOtK2SJlPvQqbsnaN2IL83DA3r4RxHtHoY0U7AsTt448FDwrLrfMEDtOrocJ2DdxI/oBpkTB3/2eMZYzcUfRruxNsq05N4V8uhRyH4/tQnMU8+LEWdGfsgpZkb3ag3sZucdojFBwacBOgS/aOms/t3cQRT4wHLAYd4nYMz5ZzGKZOxv18+C95YEXl4L3vLB1ur1C/xPpfAMYJssN7MAZah+sWi6IOZGWa4DiITEJKf8NaIcM5WyUImxT+/TGvPSdLoUb5WgOqnBaiiK+4P8n8RzvUYfMvLp6G9mV0p7yctgUwUSr7dsuwyObn7UlNuuxAYGpvsM= 15 | on: 16 | tags: true 17 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM swind/docker-python3-adb:latest 2 | -------------------------------------------------------------------------------- /HISTORY.rst: -------------------------------------------------------------------------------- 1 | 0.2.1 (2019-10-14) 2 | -------------------- 3 | 4 | * Fixes #21: Rename the package name from "adb" to "ppadb" 5 | * Fixes #23: Support push dir to device 6 | * Fixes #25: Don't call logging.basicConfig() in the module 7 | 8 | 9 | 0.1.6 (2019-01-21) 10 | ------------------- 11 | 12 | * Fix #4 push does not preserve original timestap unlike equiv adb push from command line 13 | * Fix #6 forward_list should also check serial 14 | * Fix #8: adb/command/host/__init__.py can take an exception parsing "devices" data 15 | 16 | 17 | 0.1.0 (2018-06-23) 18 | ------------------- 19 | 20 | * First release on PyPI. 21 | 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 CloudMosa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-exclude test * 2 | include README.rst 3 | include HISTORY.rst 4 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | The package name has been renamed from 'adb' to 'ppadb' 2 | ========================================================= 3 | 4 | From version **v0.2.1-dev**, the package name has been renamed from 'adb' to 'ppadb' to avoid conflit with Google `google/python-adb`_ 5 | 6 | 7 | Introduction 8 | ================== 9 | 10 | This is pure-python implementation of the ADB client. 11 | 12 | You can use it to communicate with adb server (not the adb daemon on the device/emulator). 13 | 14 | When you use `adb` command 15 | 16 | .. image:: https://raw.githubusercontent.com/Swind/pure-python-adb/master/docs/adb_cli.png 17 | 18 | Now you can use `pure-python-adb` to connect to adb server as adb command line 19 | 20 | .. image:: https://raw.githubusercontent.com/Swind/pure-python-adb/master/docs/adb_pure_python_adb.png 21 | 22 | This package supports most of the adb command line tool's functionality. 23 | 24 | 1. adb devices 25 | 2. adb shell 26 | 3. adb forward 27 | 4. adb pull/push 28 | 5. adb install/uninstall 29 | 30 | Requirements 31 | ============ 32 | 33 | Python 3.6+ 34 | 35 | Installation 36 | ============ 37 | 38 | .. code-block:: console 39 | 40 | $pip install -U pure-python-adb 41 | 42 | Examples 43 | ======== 44 | 45 | Connect to adb server and get the version 46 | ----------------------------------------- 47 | 48 | .. code-block:: python 49 | 50 | from ppadb.client import Client as AdbClient 51 | # Default is "127.0.0.1" and 5037 52 | client = AdbClient(host="127.0.0.1", port=5037) 53 | print(client.version()) 54 | 55 | >>> 39 56 | 57 | Connect to a device 58 | ------------------- 59 | 60 | .. code-block:: python 61 | 62 | from ppadb.client import Client as AdbClient 63 | # Default is "127.0.0.1" and 5037 64 | client = AdbClient(host="127.0.0.1", port=5037) 65 | device = client.device("emulator-5554") 66 | 67 | 68 | List all devices ( adb devices ) and install/uninstall an APK on all devices 69 | ---------------------------------------------------------------------------------- 70 | 71 | .. code-block:: python 72 | 73 | from ppadb.client import Client as AdbClient 74 | 75 | apk_path = "example.apk" 76 | 77 | # Default is "127.0.0.1" and 5037 78 | client = AdbClient(host="127.0.0.1", port=5037) 79 | devices = client.devices() 80 | 81 | for device in devices: 82 | device.install(apk_path) 83 | 84 | # Check apk is installed 85 | for device in devices: 86 | print(device.is_installed("example.package")) 87 | 88 | # Uninstall 89 | for device in devices: 90 | device.uninstall("example.package") 91 | 92 | adb shell 93 | --------- 94 | 95 | .. code-block:: python 96 | 97 | from ppadb.client import Client as AdbClient 98 | # Default is "127.0.0.1" and 5037 99 | client = AdbClient(host="127.0.0.1", port=5037) 100 | device = client.device("emulator-5554") 101 | device.shell("echo hello world !") 102 | 103 | .. code-block:: python 104 | 105 | def dump_logcat(connection): 106 | while True: 107 | data = connection.read(1024) 108 | if not data: 109 | break 110 | print(data.decode('utf-8')) 111 | 112 | connection.close() 113 | 114 | from ppadb.client import Client as AdbClient 115 | # Default is "127.0.0.1" and 5037 116 | client = AdbClient(host="127.0.0.1", port=5037) 117 | device = client.device("emulator-5554") 118 | device.shell("logcat", handler=dump_logcat) 119 | 120 | read logcat line by line 121 | 122 | .. code-block:: python 123 | 124 | from ppadb.client import Client 125 | 126 | def dump_logcat_by_line(connect): 127 | file_obj = connect.socket.makefile() 128 | for index in range(0, 10): 129 | print("Line {}: {}".format(index, file_obj.readline().strip())) 130 | 131 | file_obj.close() 132 | connect.close() 133 | 134 | client = Client() 135 | device = client.device("emulator-5554") 136 | device.shell("logcat", handler=dump_logcat_by_line) 137 | 138 | Screenshot 139 | ---------- 140 | 141 | .. code-block:: python 142 | 143 | from ppadb.client import Client as AdbClient 144 | client = AdbClient(host="127.0.0.1", port=5037) 145 | device = client.device("emulator-5554") 146 | result = device.screencap() 147 | with open("screen.png", "wb") as fp: 148 | fp.write(result) 149 | 150 | Push file or folder 151 | -------------------- 152 | 153 | .. code-block:: python 154 | 155 | from ppadb.client import Client as AdbClient 156 | client = AdbClient(host="127.0.0.1", port=5037) 157 | device = client.device("emulator-5554") 158 | 159 | device.push("example.apk", "/sdcard/example.apk") 160 | 161 | Pull 162 | ---- 163 | 164 | .. code-block:: python 165 | 166 | from ppadb.client import Client as AdbClient 167 | client = AdbClient(host="127.0.0.1", port=5037) 168 | device = client.device("emulator-5554") 169 | 170 | device.shell("screencap -p /sdcard/screen.png") 171 | device.pull("/sdcard/screen.png", "screen.png") 172 | 173 | Connect to device 174 | ----------------- 175 | 176 | .. code-block:: python 177 | 178 | from ppadb.client import Client as AdbClient 179 | client = AdbClient(host="127.0.0.1", port=5037) 180 | client.remote_connect("172.20.0.1", 5555) 181 | 182 | device = client.device("172.20.0.1:5555") 183 | 184 | # Disconnect all devices 185 | client.remote_disconnect() 186 | 187 | ##Disconnect 172.20.0.1 188 | # client.remote_disconnect("172.20.0.1") 189 | ##Or 190 | # client.remote_disconnect("172.20.0.1", 5555) 191 | 192 | 193 | Enable debug logger 194 | -------------------- 195 | 196 | .. code-block:: python 197 | 198 | logging.getLogger("ppadb").setLevel(logging.DEBUG) 199 | 200 | Async Client 201 | -------------------- 202 | 203 | .. code-block:: python 204 | 205 | import asyncio 206 | import aiofiles 207 | from ppadb.client_async import ClientAsync as AdbClient 208 | 209 | async def _save_screenshot(device): 210 | result = await device.screencap() 211 | file_name = f"{device.serial}.png" 212 | async with aiofiles.open(f"{file_name}", mode='wb') as f: 213 | await f.write(result) 214 | 215 | return file_name 216 | 217 | async def main(): 218 | client = AdbClient(host="127.0.0.1", port=5037) 219 | devices = await client.devices() 220 | for device in devices: 221 | print(device.serial) 222 | 223 | result = await asyncio.gather(*[_save_screenshot(device) for device in devices]) 224 | print(result) 225 | 226 | asyncio.run(main()) 227 | 228 | 229 | 230 | 231 | 232 | 233 | How to run test cases 234 | ====================== 235 | 236 | Prepare 237 | -------- 238 | 239 | 1. Install Docker 240 | 241 | 2. Install Docker Compose 242 | 243 | .. code-block:: console 244 | 245 | pip install docker-compose 246 | 247 | 3. Modify `test/conftest.py` 248 | 249 | Change the value of `adb_host` to the "emulator" 250 | 251 | .. code-block:: python 252 | 253 | adb_host="emulator" 254 | 255 | 4. Run testcases 256 | 257 | .. code-block:: console 258 | 259 | docker-compose up 260 | 261 | Result 262 | 263 | .. code-block:: console 264 | 265 | Starting purepythonadb_emulator_1 ... done 266 | Recreating purepythonadb_python_environment_1 ... done 267 | Attaching to purepythonadb_emulator_1, purepythonadb_python_environment_1 268 | emulator_1 | + echo n 269 | emulator_1 | + /home/user/android-sdk-linux/tools/bin/avdmanager create avd -k system-images;android-25;google_apis;x86 -n Docker -b x86 -g google_apis --device 8 --force 270 | Parsing /home/user/android-sdk-linux/emulator/package.xmlParsing /home/user/android-sdk-linux/patcher/v4/package.xmlParsing /home/user/android-sdk-linux/platform-tools/package.xmlParsing /home/user/android-sdk-linux/platforms/android-25/package.xmlParsing /home/user/android-sdk-linux/system-images/android-25/google_apis/x86/package.xmlParsing /home/user/android-sdk-linux/tools/package.xml+ echo hw.keyboard = true 271 | emulator_1 | + adb start-server 272 | emulator_1 | * daemon not running; starting now at tcp:5037 273 | python_environment_1 | ============================= test session starts ============================== 274 | python_environment_1 | platform linux -- Python 3.6.1, pytest-3.6.3, py-1.5.4, pluggy-0.6.0 275 | python_environment_1 | rootdir: /code, inifile: 276 | python_environment_1 | collected 27 items 277 | python_environment_1 | 278 | emulator_1 | * daemon started successfully 279 | emulator_1 | + exec /usr/bin/supervisord 280 | emulator_1 | /usr/lib/python2.7/dist-packages/supervisor/options.py:298: UserWarning: Supervisord is running as root and it is searching for its configuration file in default locations (including its current working directory); you probably want to specify a "-c" argument specifying an absolute path to a configuration file for improved security. 281 | emulator_1 | 'Supervisord is running as root and it is searching ' 282 | emulator_1 | 2018-07-07 17:19:47,560 CRIT Supervisor running as root (no user in config file) 283 | emulator_1 | 2018-07-07 17:19:47,560 INFO Included extra file "/etc/supervisor/conf.d/supervisord.conf" during parsing 284 | emulator_1 | 2018-07-07 17:19:47,570 INFO RPC interface 'supervisor' initialized 285 | emulator_1 | 2018-07-07 17:19:47,570 CRIT Server 'unix_http_server' running without any HTTP authentication checking 286 | emulator_1 | 2018-07-07 17:19:47,570 INFO supervisord started with pid 1 287 | emulator_1 | 2018-07-07 17:19:48,573 INFO spawned: 'socat-5554' with pid 74 288 | emulator_1 | 2018-07-07 17:19:48,574 INFO spawned: 'socat-5555' with pid 75 289 | emulator_1 | 2018-07-07 17:19:48,576 INFO spawned: 'socat-5037' with pid 76 290 | emulator_1 | 2018-07-07 17:19:48,578 INFO spawned: 'novnc' with pid 77 291 | emulator_1 | 2018-07-07 17:19:48,579 INFO spawned: 'socat-9008' with pid 78 292 | emulator_1 | 2018-07-07 17:19:48,582 INFO spawned: 'emulator' with pid 80 293 | emulator_1 | 2018-07-07 17:19:49,607 INFO success: socat-5554 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs) 294 | emulator_1 | 2018-07-07 17:19:49,607 INFO success: socat-5555 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs) 295 | emulator_1 | 2018-07-07 17:19:49,607 INFO success: socat-5037 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs) 296 | emulator_1 | 2018-07-07 17:19:49,607 INFO success: novnc entered RUNNING state, process has stayed up for > than 1 seconds (startsecs) 297 | emulator_1 | 2018-07-07 17:19:49,608 INFO success: socat-9008 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs) 298 | emulator_1 | 2018-07-07 17:19:49,608 INFO success: emulator entered RUNNING state, process has stayed up for > than 1 seconds (startsecs) 299 | python_environment_1 | test/test_device.py .............. [ 51%] 300 | python_environment_1 | test/test_host.py .. [ 59%] 301 | python_environment_1 | test/test_host_serial.py ........ [ 88%] 302 | python_environment_1 | test/test_plugins.py ... [100%] 303 | python_environment_1 | 304 | python_environment_1 | ------------------ generated xml file: /code/test_result.xml ------------------- 305 | python_environment_1 | ========================= 27 passed in 119.15 seconds ========================== 306 | purepythonadb_python_environment_1 exited with code 0 307 | Aborting on container exit... 308 | Stopping purepythonadb_emulator_1 ... done 309 | 310 | More Information 311 | ================= 312 | 313 | A pure Node.js client for the Android Debug Bridge 314 | --------------------------------------------------- 315 | 316 | adbkit_ 317 | 318 | ADB documents 319 | -------------- 320 | 321 | - protocol_ 322 | - services_ 323 | - sync_ 324 | 325 | .. _adbkit: https://github.com/openstf/stf 326 | .. _protocol: https://android.googlesource.com/platform/system/core/+/master/adb/protocol.txt 327 | .. _services: https://android.googlesource.com/platform/system/core/+/master/adb/SERVICES.TXT 328 | .. _sync: https://android.googlesource.com/platform/system/core/+/master/adb/SYNC.TXT 329 | .. _`google/python-adb`: https://github.com/google/python-adb 330 | -------------------------------------------------------------------------------- /bumpversion.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | bump2version --no-tag minor ppadb/__init__.py 4 | 5 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | python_environment: 4 | image: python:3 5 | depends_on: 6 | - emulator 7 | volumes: 8 | - .:/code 9 | working_dir: /code 10 | environment: 11 | - PYTHONPATH=/code 12 | - PYTHONUNBUFFERED=0 13 | command: sh -c "pip install -r requirements.txt;py.test test -s -v --junit-xml test_result.xml;pip install 'aiofiles>=0.4.0';py.test test_async -s -v --junit-xml test_result_async.xml" 14 | 15 | emulator: 16 | image: swind/android-emulator:android_28 17 | environment: 18 | - ANDROID_AVD_EXTRA_ARGS=--device 8 --force 19 | - ANDROID_EMULATOR_EXTRA_ARGS=-skin 1080x1920 -memory 2048 -no-boot-anim -gpu host -qemu 20 | ports: 21 | - 6080:6080 22 | devices: 23 | - "/dev/kvm:/dev/kvm" 24 | -------------------------------------------------------------------------------- /docs/adb_cli.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swind/pure-python-adb/b136202b04660db57b49418514dd9eddc2ecb365/docs/adb_cli.png -------------------------------------------------------------------------------- /docs/adb_pure_python_adb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swind/pure-python-adb/b136202b04660db57b49418514dd9eddc2ecb365/docs/adb_pure_python_adb.png -------------------------------------------------------------------------------- /example/screencap.py: -------------------------------------------------------------------------------- 1 | from ppadb.client import Client as AdbClient 2 | 3 | client = AdbClient(host="127.0.0.1", port=5037) 4 | device = client.device("emulator-5554") 5 | 6 | device.push("./screencap.py", "/sdcard/screencap.py") 7 | 8 | 9 | -------------------------------------------------------------------------------- /ppadb/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.3.0-dev" 2 | 3 | class InstallError(Exception): 4 | def __init__(self, path, error): 5 | super(InstallError, self).__init__("{} could not be installed - [{}]".format(path, error)) 6 | 7 | 8 | class ClearError(Exception): 9 | def __init__(self, package, error): 10 | super(ClearError, self).__init__("Package {} could not be cleared - [{}]".format(package, error)) 11 | -------------------------------------------------------------------------------- /ppadb/application.py: -------------------------------------------------------------------------------- 1 | class Application: 2 | def __init__(self, device, package): 3 | self._device = device 4 | self._package = package 5 | 6 | def pid(self): 7 | pass 8 | 9 | def uid(self): 10 | pass 11 | 12 | @property 13 | def tcp_recv(self): 14 | return 0 15 | 16 | @property 17 | def tcp_send(self): 18 | return 0 19 | -------------------------------------------------------------------------------- /ppadb/client.py: -------------------------------------------------------------------------------- 1 | from ppadb.command.host import Host 2 | from ppadb.connection import Connection 3 | from ppadb.utils.logger import AdbLogging 4 | 5 | logger = AdbLogging.get_logger(__name__) 6 | 7 | class Client(Host): 8 | def __init__(self, host='127.0.0.1', port=5037): 9 | self.host = host 10 | self.port = port 11 | 12 | def create_connection(self, timeout=None): 13 | conn = Connection(self.host, self.port, timeout) 14 | conn.connect() 15 | return conn 16 | 17 | def device(self, serial): 18 | devices = self.devices() 19 | 20 | for device in devices: 21 | if device.serial == serial: 22 | return device 23 | 24 | return None 25 | -------------------------------------------------------------------------------- /ppadb/client_async.py: -------------------------------------------------------------------------------- 1 | from ppadb.command.host_async import HostAsync 2 | from ppadb.connection_async import ConnectionAsync 3 | 4 | 5 | class ClientAsync(HostAsync): 6 | def __init__(self, host='127.0.0.1', port=5037): 7 | self.host = host 8 | self.port = port 9 | 10 | async def create_connection(self, timeout=None): 11 | conn = ConnectionAsync(self.host, self.port, timeout) 12 | return await conn.connect() 13 | 14 | async def device(self, serial): 15 | devices = await self.devices() 16 | 17 | for device in devices: 18 | if device.serial == serial: 19 | return device 20 | 21 | return None 22 | -------------------------------------------------------------------------------- /ppadb/command/__init__.py: -------------------------------------------------------------------------------- 1 | class Command: 2 | def create_connection(self, *args, **kwargs): 3 | return None 4 | -------------------------------------------------------------------------------- /ppadb/command/host/__init__.py: -------------------------------------------------------------------------------- 1 | from ppadb.device import Device 2 | from ppadb.command import Command 3 | 4 | 5 | class Host(Command): 6 | CONNECT_RESULT_PATTERN = "(connected to|already connected)" 7 | 8 | OFFLINE = "offline" 9 | DEVICE = "device" 10 | BOOTLOADER = "bootloader" 11 | 12 | def _execute_cmd(self, cmd, with_response=True): 13 | with self.create_connection() as conn: 14 | conn.send(cmd) 15 | if with_response: 16 | result = conn.receive() 17 | return result 18 | else: 19 | conn.check_status() 20 | 21 | def devices(self, state=None): 22 | cmd = "host:devices" 23 | result = self._execute_cmd(cmd) 24 | 25 | devices = [] 26 | 27 | for line in result.split('\n'): 28 | if not line: 29 | break 30 | 31 | tokens = line.split() 32 | if state and len(tokens) > 1 and tokens[1] != state: 33 | continue 34 | 35 | devices.append(Device(self, tokens[0])) 36 | 37 | return devices 38 | 39 | def features(self): 40 | cmd = "host:features" 41 | result = self._execute_cmd(cmd) 42 | features = result.split(",") 43 | return features 44 | 45 | def version(self): 46 | with self.create_connection() as conn: 47 | conn.send("host:version") 48 | version = conn.receive() 49 | return int(version, 16) 50 | 51 | def kill(self): 52 | """ 53 | Ask the ADB server to quit immediately. This is used when the 54 | ADB client detects that an obsolete server is running after an 55 | upgrade. 56 | """ 57 | with self.create_connection() as conn: 58 | conn.send("host:kill") 59 | 60 | return True 61 | 62 | def killforward_all(self): 63 | cmd = "host:killforward-all" 64 | self._execute_cmd(cmd, with_response=False) 65 | 66 | def list_forward(self): 67 | cmd = "host:list-forward" 68 | result = self._execute_cmd(cmd) 69 | 70 | device_forward_map = {} 71 | for line in result.split('\n'): 72 | if line: 73 | serial, local, remote = line.split() 74 | if serial not in device_forward_map: 75 | device_forward_map[serial] = {} 76 | 77 | device_forward_map[serial][local] = remote 78 | 79 | return device_forward_map 80 | 81 | def remote_connect(self, host, port): 82 | cmd = "host:connect:%s:%d" % (host, port) 83 | result = self._execute_cmd(cmd) 84 | 85 | return "connected" in result 86 | 87 | def remote_disconnect(self, host=None, port=None): 88 | cmd = "host:disconnect:" 89 | if host: 90 | cmd = "host:disconnect:{}".format(host) 91 | if port: 92 | cmd = "{}:{}".format(cmd, port) 93 | 94 | return self._execute_cmd(cmd) 95 | -------------------------------------------------------------------------------- /ppadb/command/host_async/__init__.py: -------------------------------------------------------------------------------- 1 | from ppadb.device_async import DeviceAsync 2 | 3 | 4 | class HostAsync: 5 | CONNECT_RESULT_PATTERN = "(connected to|already connected)" 6 | 7 | OFFLINE = "offline" 8 | DEVICE = "device" 9 | BOOTLOADER = "bootloader" 10 | 11 | async def _execute_cmd(self, cmd): 12 | async with await self.create_connection() as conn: 13 | await conn.send(cmd) 14 | return await conn.receive() 15 | 16 | async def devices(self): 17 | cmd = "host:devices" 18 | result = await self._execute_cmd(cmd) 19 | 20 | devices = [] 21 | 22 | for line in result.split('\n'): 23 | if not line: 24 | break 25 | 26 | devices.append(DeviceAsync(self, line.split()[0])) 27 | 28 | return devices 29 | -------------------------------------------------------------------------------- /ppadb/command/serial/__init__.py: -------------------------------------------------------------------------------- 1 | from ppadb.command import Command 2 | 3 | 4 | class Serial(Command): 5 | def _execute_cmd(self, cmd, with_response=True): 6 | conn = self.create_connection(set_transport=False) 7 | 8 | with conn: 9 | conn.send(cmd) 10 | if with_response: 11 | result = conn.receive() 12 | return result 13 | else: 14 | conn.check_status() 15 | 16 | def forward(self, local, remote, norebind=False): 17 | if norebind: 18 | cmd = "host-serial:{serial}:forward:norebind:{local};{remote}".format( 19 | serial=self.serial, 20 | local=local, 21 | remote=remote) 22 | else: 23 | cmd = "host-serial:{serial}:forward:{local};{remote}".format( 24 | serial=self.serial, 25 | local=local, 26 | remote=remote) 27 | 28 | self._execute_cmd(cmd, with_response=False) 29 | 30 | def list_forward(self): 31 | # According to https://android.googlesource.com/platform/system/core/+/master/adb/adb_listeners.cpp#129 32 | # And https://android.googlesource.com/platform/system/core/+/master/adb/SERVICES.TXT#130 33 | # The 'list-forward' always lists all existing forward connections from the adb server 34 | # So we need filter these by self. 35 | cmd = "host-serial:{serial}:list-forward".format(serial=self.serial) 36 | result = self._execute_cmd(cmd) 37 | 38 | forward_map = {} 39 | 40 | for line in result.split('\n'): 41 | if line: 42 | serial, local, remote = line.split() 43 | if serial == self.serial: 44 | forward_map[local] = remote 45 | 46 | return forward_map 47 | 48 | def killforward(self, local): 49 | cmd = "host-serial:{serial}:killforward:{local}".format(serial=self.serial, local=local) 50 | self._execute_cmd(cmd, with_response=False) 51 | 52 | def killforward_all(self): 53 | # killforward-all command ignores the and remove all the forward mapping. 54 | # So we need to implement this function by self 55 | forward_map = self.list_forward() 56 | for local, remote in forward_map.items(): 57 | self.killforward(local) 58 | 59 | def get_device_path(self): 60 | cmd = "host-serial:{serial}:get-devpath".format(serial=self.serial) 61 | return self._execute_cmd(cmd) 62 | 63 | def get_serial_no(self): 64 | cmd = "host-serial:{serial}:get-serialno".format(serial=self.serial) 65 | return self._execute_cmd(cmd) 66 | 67 | def get_state(self): 68 | cmd = "host-serial:{serial}:get-state".format(serial=self.serial) 69 | return self._execute_cmd(cmd) 70 | -------------------------------------------------------------------------------- /ppadb/command/transport/__init__.py: -------------------------------------------------------------------------------- 1 | import re 2 | import time 3 | 4 | from ppadb import ClearError 5 | from ppadb.command import Command 6 | 7 | from ppadb.utils.logger import AdbLogging 8 | 9 | logger = AdbLogging.get_logger(__name__) 10 | 11 | 12 | class Transport(Command): 13 | def transport(self, connection): 14 | cmd = "host:transport:{}".format(self.serial) 15 | connection.send(cmd) 16 | 17 | return connection 18 | 19 | def shell(self, cmd, handler=None, timeout=None): 20 | conn = self.create_connection(timeout=timeout) 21 | 22 | cmd = "shell:{}".format(cmd) 23 | conn.send(cmd) 24 | 25 | if handler: 26 | handler(conn) 27 | else: 28 | result = conn.read_all() 29 | conn.close() 30 | return result.decode('utf-8') 31 | 32 | def sync(self): 33 | conn = self.create_connection() 34 | 35 | cmd = "sync:" 36 | conn.send(cmd) 37 | 38 | return conn 39 | 40 | def screencap(self): 41 | conn = self.create_connection() 42 | 43 | with conn: 44 | cmd = "shell:/system/bin/screencap -p" 45 | conn.send(cmd) 46 | result = conn.read_all() 47 | 48 | if result and len(result) > 5 and result[5] == 0x0d: 49 | return result.replace(b'\r\n', b'\n') 50 | else: 51 | return result 52 | 53 | def clear(self, package): 54 | clear_result_pattern = "(Success|Failed)" 55 | 56 | result = self.shell("pm clear {}".format(package)) 57 | m = re.search(clear_result_pattern, result) 58 | 59 | if m is not None and m.group(1) == "Success": 60 | return True 61 | else: 62 | logger.error(result) 63 | raise ClearError(package, result.strip()) 64 | 65 | def framebuffer(self): 66 | raise NotImplemented() 67 | 68 | def list_features(self): 69 | result = self.shell("pm list features 2>/dev/null") 70 | 71 | result_pattern = "^feature:(.*?)(?:=(.*?))?\r?$" 72 | features = {} 73 | for line in result.split('\n'): 74 | m = re.match(result_pattern, line) 75 | if m: 76 | value = True if m.group(2) is None else m.group(2) 77 | features[m.group(1)] = value 78 | 79 | return features 80 | 81 | def list_packages(self): 82 | result = self.shell("pm list packages 2>/dev/null") 83 | result_pattern = "^package:(.*?)\r?$" 84 | 85 | packages = [] 86 | for line in result.split('\n'): 87 | m = re.match(result_pattern, line) 88 | if m: 89 | packages.append(m.group(1)) 90 | 91 | return packages 92 | 93 | def get_properties(self): 94 | result = self.shell("getprop") 95 | result_pattern = "^\[([\s\S]*?)\]: \[([\s\S]*?)\]\r?$" 96 | 97 | properties = {} 98 | for line in result.split('\n'): 99 | m = re.match(result_pattern, line) 100 | if m: 101 | properties[m.group(1)] = m.group(2) 102 | 103 | return properties 104 | 105 | def list_reverses(self): 106 | conn = self.create_connection() 107 | with conn: 108 | cmd = "reverse:list-forward" 109 | conn.send(cmd) 110 | result = conn.receive() 111 | 112 | reverses = [] 113 | for line in result.split('\n'): 114 | if not line: 115 | continue 116 | 117 | serial, remote, local = line.split() 118 | reverses.append( 119 | { 120 | 'remote': remote, 121 | 'local': local 122 | } 123 | ) 124 | 125 | return reverses 126 | 127 | def local(self, path): 128 | if ":" not in path: 129 | path = "localfilesystem:{}".format(path) 130 | 131 | conn = self.create_connection() 132 | conn.send(path) 133 | 134 | return conn 135 | 136 | def log(self, name): 137 | conn = self.create_connection() 138 | cmd = "log:{}".format(name) 139 | 140 | conn.send(cmd) 141 | 142 | return conn 143 | 144 | def logcat(self, clear=False): 145 | raise NotImplemented() 146 | 147 | def reboot(self): 148 | conn = self.create_connection() 149 | 150 | with conn: 151 | conn.send("reboot:") 152 | conn.read_all() 153 | 154 | return True 155 | 156 | def remount(self): 157 | conn = self.create_connection() 158 | 159 | with conn: 160 | conn.send("remount:") 161 | 162 | return True 163 | 164 | def reverse(self, remote, local): 165 | cmd = "reverse:forward:{remote}:{local}".format( 166 | remote=remote, 167 | local=local 168 | ) 169 | 170 | conn = self.create_connection() 171 | with conn: 172 | conn.send(cmd) 173 | 174 | # Check status again, the first check is send cmd status, the second time is check the forward status. 175 | conn.check_status() 176 | 177 | return True 178 | 179 | def root(self): 180 | # Restarting adbd as root 181 | conn = self.create_connection() 182 | with conn: 183 | conn.send("root:") 184 | result = conn.read_all().decode('utf-8') 185 | 186 | if "restarting adbd as root" in result: 187 | return True 188 | else: 189 | raise RuntimeError(result.strip()) 190 | 191 | def wait_boot_complete(self, timeout=60, timedelta=1): 192 | """ 193 | :param timeout: second 194 | :param timedelta: second 195 | """ 196 | cmd = 'getprop sys.boot_completed' 197 | 198 | end_time = time.time() + timeout 199 | 200 | while True: 201 | try: 202 | result = self.shell(cmd) 203 | except RuntimeError as e: 204 | logger.warning(e) 205 | continue 206 | 207 | if result.strip() == "1": 208 | return True 209 | 210 | if time.time() > end_time: 211 | raise TimeoutError() 212 | elif timedelta > 0: 213 | time.sleep(timedelta) 214 | -------------------------------------------------------------------------------- /ppadb/command/transport_async/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import re 3 | import time 4 | 5 | 6 | class TransportAsync: 7 | async def transport(self, connection): 8 | cmd = "host:transport:{}".format(self.serial) 9 | await connection.send(cmd) 10 | 11 | return connection 12 | 13 | async def shell(self, cmd, timeout=None): 14 | conn = await self.create_connection(timeout=timeout) 15 | 16 | cmd = "shell:{}".format(cmd) 17 | await conn.send(cmd) 18 | 19 | result = await conn.read_all() 20 | await conn.close() 21 | return result.decode('utf-8') 22 | 23 | async def sync(self): 24 | conn = await self.create_connection() 25 | 26 | cmd = "sync:" 27 | await conn.send(cmd) 28 | 29 | return conn 30 | 31 | async def screencap(self): 32 | async with await self.create_connection() as conn: 33 | cmd = "shell:/system/bin/screencap -p" 34 | await conn.send(cmd) 35 | result = await conn.read_all() 36 | 37 | if result and len(result) > 5 and result[5] == 0x0d: 38 | return result.replace(b'\r\n', b'\n') 39 | else: 40 | return result 41 | -------------------------------------------------------------------------------- /ppadb/connection.py: -------------------------------------------------------------------------------- 1 | import struct 2 | import socket 3 | 4 | from ppadb.protocol import Protocol 5 | from ppadb.utils.logger import AdbLogging 6 | 7 | logger = AdbLogging.get_logger(__name__) 8 | 9 | 10 | class Connection: 11 | def __init__(self, host='localhost', port=5037, timeout=None): 12 | self.host = host 13 | self.port = port 14 | self.timeout = timeout 15 | self.socket = None 16 | 17 | def __enter__(self): 18 | return self 19 | 20 | def __exit__(self, type, value, traceback): 21 | self.close() 22 | 23 | def connect(self): 24 | logger.debug("Connect to adb server - {}:{}".format(self.host, self.port)) 25 | 26 | self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 27 | 28 | l_onoff = 1 29 | l_linger = 0 30 | 31 | self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack('ii', l_onoff, l_linger)) 32 | if self.timeout: 33 | self.socket.settimeout(self.timeout) 34 | 35 | try: 36 | self.socket.connect((self.host, self.port)) 37 | except socket.error as e: 38 | self.close() 39 | raise RuntimeError("ERROR: connecting to {}:{} {}.\nIs adb running on your computer?".format( 40 | self.host, 41 | self.port, 42 | e 43 | )) 44 | 45 | return self.socket 46 | 47 | def close(self): 48 | if not self.socket: 49 | return 50 | 51 | logger.debug("Connection closed...") 52 | try: 53 | self.socket.close() 54 | except OSError: 55 | pass 56 | 57 | ############################################################################################################## 58 | # 59 | # Send command & Receive command result 60 | # 61 | ############################################################################################################## 62 | def _recv(self, length): 63 | return self.socket.recv(length) 64 | 65 | def _recv_into(self, length): 66 | recv = bytearray(length) 67 | view = memoryview(recv) 68 | self.socket.recv_into(view) 69 | return recv 70 | 71 | def _send(self, data): 72 | self.socket.send(data) 73 | 74 | def receive(self): 75 | nob = int(self._recv(4).decode('utf-8'), 16) 76 | recv = self._recv_into(nob) 77 | 78 | return recv.decode('utf-8') 79 | 80 | def send(self, msg): 81 | msg = Protocol.encode_data(msg) 82 | logger.debug(msg) 83 | self._send(msg) 84 | return self._check_status() 85 | 86 | def _check_status(self): 87 | recv = self._recv(4).decode('utf-8') 88 | if recv != Protocol.OKAY: 89 | error = self._recv(1024).decode('utf-8') 90 | raise RuntimeError("ERROR: {} {}".format(repr(recv), error)) 91 | 92 | return True 93 | 94 | def check_status(self): 95 | return self._check_status() 96 | 97 | ############################################################################################################## 98 | # 99 | # Socket read/write 100 | # 101 | ############################################################################################################## 102 | def read_all(self): 103 | data = bytearray() 104 | 105 | while True: 106 | recv = self._recv(4096) 107 | if not recv: 108 | break 109 | data += recv 110 | 111 | return data 112 | 113 | def read(self, length=0): 114 | data = self._recv(length) 115 | return data 116 | 117 | def write(self, data): 118 | self._send(data) 119 | -------------------------------------------------------------------------------- /ppadb/connection_async.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import struct 3 | import socket 4 | 5 | from ppadb.protocol import Protocol 6 | from ppadb.utils.logger import AdbLogging 7 | 8 | logger = AdbLogging.get_logger(__name__) 9 | 10 | 11 | class ConnectionAsync: 12 | def __init__(self, host='localhost', port=5037, timeout=None): 13 | self.host = host 14 | self.port = int(port) 15 | self.timeout = timeout 16 | 17 | self.reader = None 18 | self.writer = None 19 | 20 | async def __aenter__(self): 21 | return self 22 | 23 | async def __aexit__(self, type, value, traceback): 24 | await self.close() 25 | 26 | async def connect(self): 27 | logger.debug("Connect to ADB server - %s:%d", self.host, self.port) 28 | 29 | try: 30 | if self.timeout: 31 | self.reader, self.writer = await asyncio.wait_for(asyncio.open_connection(self.host, self.port), self.timeout) 32 | else: 33 | self.reader, self.writer = await asyncio.open_connection(self.host, self.port) 34 | 35 | except (OSError, asyncio.TimeoutError) as e: 36 | raise RuntimeError("ERROR: connecting to {}:{} {}.\nIs adb running on your computer?".format(self.host, self.port, e)) 37 | 38 | return self 39 | 40 | async def close(self): 41 | logger.debug("Connection closed...") 42 | 43 | if self.writer: 44 | try: 45 | self.writer.close() 46 | await self.writer.wait_closed() 47 | except OSError: 48 | pass 49 | 50 | self.reader = None 51 | self.writer = None 52 | 53 | ############################################################################################################## 54 | # 55 | # Send command & Receive command result 56 | # 57 | ############################################################################################################## 58 | async def _recv(self, length): 59 | return await asyncio.wait_for(self.reader.read(length), self.timeout) 60 | 61 | async def _send(self, data): 62 | self.writer.write(data) 63 | await asyncio.wait_for(self.writer.drain(), self.timeout) 64 | 65 | async def receive(self): 66 | nob = int((await self._recv(4)).decode('utf-8'), 16) 67 | return (await self._recv(nob)).decode('utf-8') 68 | 69 | async def send(self, msg): 70 | msg = Protocol.encode_data(msg) 71 | logger.debug(msg) 72 | await self._send(msg) 73 | return await self._check_status() 74 | 75 | async def _check_status(self): 76 | recv = (await self._recv(4)).decode('utf-8') 77 | if recv != Protocol.OKAY: 78 | error = (await self._recv(1024)).decode('utf-8') 79 | raise RuntimeError("ERROR: {} {}".format(repr(recv), error)) 80 | 81 | return True 82 | 83 | ############################################################################################################## 84 | # 85 | # Socket read/write 86 | # 87 | ############################################################################################################## 88 | async def read_all(self): 89 | data = bytearray() 90 | 91 | while True: 92 | recv = await self._recv(4096) 93 | if not recv: 94 | break 95 | data += recv 96 | 97 | return data 98 | 99 | async def read(self, length=0): 100 | data = await self._recv(length) 101 | return data 102 | 103 | async def write(self, data): 104 | await self._send(data) 105 | -------------------------------------------------------------------------------- /ppadb/device.py: -------------------------------------------------------------------------------- 1 | import re 2 | import os 3 | 4 | from ppadb.command.transport import Transport 5 | from ppadb.command.serial import Serial 6 | 7 | from ppadb.plugins.device.input import Input 8 | from ppadb.plugins.device.utils import Utils 9 | from ppadb.plugins.device.wm import WM 10 | from ppadb.plugins.device.traffic import Traffic 11 | from ppadb.plugins.device.cpustat import CPUStat 12 | from ppadb.plugins.device.batterystats import BatteryStats 13 | 14 | from ppadb.sync import Sync 15 | 16 | from ppadb.utils.logger import AdbLogging 17 | 18 | from ppadb import InstallError 19 | 20 | logger = AdbLogging.get_logger(__name__) 21 | 22 | try: 23 | FileNotFoundError 24 | except NameError: 25 | FileNotFoundError = IOError 26 | 27 | try: 28 | from shlex import quote as cmd_quote 29 | except ImportError: 30 | from pipes import quote as cmd_quote 31 | 32 | 33 | class Device(Transport, Serial, Input, Utils, WM, Traffic, CPUStat, BatteryStats): 34 | INSTALL_RESULT_PATTERN = "(Success|Failure|Error)\s?(.*)" 35 | UNINSTALL_RESULT_PATTERN = "(Success|Failure.*|.*Unknown package:.*)" 36 | 37 | def __init__(self, client, serial): 38 | self.client = client 39 | self.serial = serial 40 | 41 | def create_connection(self, set_transport=True, timeout=None): 42 | conn = self.client.create_connection(timeout=timeout) 43 | 44 | if set_transport: 45 | self.transport(conn) 46 | 47 | return conn 48 | 49 | def _push(self, src, dest, mode, progress): 50 | # Create a new connection for file transfer 51 | sync_conn = self.sync() 52 | sync = Sync(sync_conn) 53 | 54 | with sync_conn: 55 | sync.push(src, dest, mode, progress) 56 | 57 | def push(self, src, dest, mode=0o644, progress=None): 58 | if not os.path.exists(src): 59 | raise FileNotFoundError("Cannot find {}".format(src)) 60 | elif os.path.isfile(src): 61 | self._push(src, dest, mode, progress) 62 | elif os.path.isdir(src): 63 | 64 | basename = os.path.basename(src) 65 | 66 | for root, dirs, files in os.walk(src): 67 | subdir = root.replace(src, "") 68 | if subdir.startswith("/"): 69 | subdir = subdir[1:] 70 | root_dir_path = os.path.join(basename, subdir) 71 | 72 | self.shell("mkdir -p {}/{}".format(dest, root_dir_path)) 73 | 74 | for item in files: 75 | self._push(os.path.join(root, item), os.path.join(dest, root_dir_path, item), mode, progress) 76 | 77 | def pull(self, src, dest): 78 | sync_conn = self.sync() 79 | sync = Sync(sync_conn) 80 | 81 | with sync_conn: 82 | return sync.pull(src, dest) 83 | 84 | def install(self, path, 85 | forward_lock=False, # -l 86 | reinstall=False, # -r 87 | test=False, # -t 88 | installer_package_name="", # -i {installer_package_name} 89 | shared_mass_storage=False, # -s 90 | internal_system_memory=False, # -f 91 | downgrade=False, # -d 92 | grand_all_permissions=False # -g 93 | ): 94 | dest = Sync.temp(path) 95 | self.push(path, dest) 96 | 97 | parameters = [] 98 | if forward_lock: parameters.append("-l") 99 | if reinstall: parameters.append("-r") 100 | if test: parameters.append("-t") 101 | if len(installer_package_name) > 0: parameters.append("-i {}".format(installer_package_name)) 102 | if shared_mass_storage: parameters.append("-s") 103 | if internal_system_memory: parameters.append("-f") 104 | if downgrade: parameters.append("-d") 105 | if grand_all_permissions: parameters.append("-g") 106 | 107 | try: 108 | result = self.shell("pm install {} {}".format(" ".join(parameters), cmd_quote(dest))) 109 | match = re.search(self.INSTALL_RESULT_PATTERN, result) 110 | 111 | if match and match.group(1) == "Success": 112 | return True 113 | elif match: 114 | groups = match.groups() 115 | raise InstallError(dest, groups[1]) 116 | else: 117 | raise InstallError(dest, result) 118 | finally: 119 | self.shell("rm -f {}".format(dest)) 120 | 121 | def is_installed(self, package): 122 | result = self.shell('pm path {}'.format(package)) 123 | 124 | if "package:" in result: 125 | return True 126 | else: 127 | return False 128 | 129 | def uninstall(self, package): 130 | result = self.shell('pm uninstall {}'.format(package)) 131 | 132 | m = re.search(self.UNINSTALL_RESULT_PATTERN, result) 133 | 134 | if m and m.group(1) == "Success": 135 | return True 136 | elif m: 137 | logger.error(m.group(1)) 138 | return False 139 | else: 140 | logger.error("There is no message after uninstalling") 141 | return False 142 | -------------------------------------------------------------------------------- /ppadb/device_async.py: -------------------------------------------------------------------------------- 1 | try: 2 | from asyncio import get_running_loop 3 | except ImportError: # pragma: no cover 4 | from asyncio import get_event_loop as get_running_loop # Python 3.6 compatibility 5 | 6 | import re 7 | import os 8 | 9 | from ppadb.command.transport_async import TransportAsync 10 | from ppadb.sync_async import SyncAsync 11 | 12 | 13 | def _get_src_info(src): 14 | exists = os.path.exists(src) 15 | isfile = os.path.isfile(src) 16 | isdir = os.path.isdir(src) 17 | basename = os.path.basename(src) 18 | walk = None if not isdir else list(os.walk(src)) 19 | 20 | return exists, isfile, isdir, basename, walk 21 | 22 | 23 | class DeviceAsync(TransportAsync): 24 | INSTALL_RESULT_PATTERN = "(Success|Failure|Error)\s?(.*)" 25 | UNINSTALL_RESULT_PATTERN = "(Success|Failure.*|.*Unknown package:.*)" 26 | 27 | def __init__(self, client, serial): 28 | self.client = client 29 | self.serial = serial 30 | 31 | async def create_connection(self, set_transport=True, timeout=None): 32 | conn = await self.client.create_connection(timeout=timeout) 33 | 34 | if set_transport: 35 | await self.transport(conn) 36 | 37 | return conn 38 | 39 | async def _push(self, src, dest, mode, progress): 40 | # Create a new connection for file transfer 41 | sync_conn = await self.sync() 42 | sync = SyncAsync(sync_conn) 43 | 44 | async with sync_conn: 45 | await sync.push(src, dest, mode, progress) 46 | 47 | async def push(self, src, dest, mode=0o644, progress=None): 48 | exists, isfile, isdir, basename, walk = await get_running_loop().run_in_executor(None, _get_src_info, src) 49 | if not exists: 50 | raise FileNotFoundError("Cannot find {}".format(src)) 51 | 52 | if isfile: 53 | await self._push(src, dest, mode, progress) 54 | 55 | elif isdir: 56 | for root, dirs, files in walk: 57 | root_dir_path = os.path.join(basename, root.replace(src, "")) 58 | 59 | await self.shell("mkdir -p {}/{}".format(dest, root_dir_path)) 60 | 61 | for item in files: 62 | await self._push(os.path.join(root, item), os.path.join(dest, root_dir_path, item), mode, progress) 63 | 64 | async def pull(self, src, dest): 65 | sync_conn = await self.sync() 66 | sync = SyncAsync(sync_conn) 67 | 68 | async with sync_conn: 69 | return await sync.pull(src, dest) 70 | -------------------------------------------------------------------------------- /ppadb/keycode.py: -------------------------------------------------------------------------------- 1 | KEYCODE_UNKNOWN = 0 2 | KEYCODE_SOFT_LEFT = 1 3 | KEYCODE_SOFT_RIGHT = 2 4 | KEYCODE_HOME = 3 5 | KEYCODE_BACK = 4 6 | KEYCODE_CALL = 5 7 | KEYCODE_ENDCALL = 6 8 | KEYCODE_0 = 7 9 | KEYCODE_1 = 8 10 | KEYCODE_2 = 9 11 | KEYCODE_3 = 10 12 | KEYCODE_4 = 11 13 | KEYCODE_5 = 12 14 | KEYCODE_6 = 13 15 | KEYCODE_7 = 14 16 | KEYCODE_8 = 15 17 | KEYCODE_9 = 16 18 | KEYCODE_STAR = 17 19 | KEYCODE_POUND = 18 20 | KEYCODE_DPAD_UP = 19 21 | KEYCODE_DPAD_DOWN = 20 22 | KEYCODE_DPAD_LEFT = 21 23 | KEYCODE_DPAD_RIGHT = 22 24 | KEYCODE_DPAD_CENTER = 23 25 | KEYCODE_VOLUME_UP = 24 26 | KEYCODE_VOLUME_DOWN = 25 27 | KEYCODE_POWER = 26 28 | KEYCODE_CAMERA = 27 29 | KEYCODE_CLEAR = 28 30 | KEYCODE_A = 29 31 | KEYCODE_B = 30 32 | KEYCODE_C = 31 33 | KEYCODE_D = 32 34 | KEYCODE_E = 33 35 | KEYCODE_F = 34 36 | KEYCODE_G = 35 37 | KEYCODE_H = 36 38 | KEYCODE_I = 37 39 | KEYCODE_J = 38 40 | KEYCODE_K = 39 41 | KEYCODE_L = 40 42 | KEYCODE_M = 41 43 | KEYCODE_N = 42 44 | KEYCODE_O = 43 45 | KEYCODE_P = 44 46 | KEYCODE_Q = 45 47 | KEYCODE_R = 46 48 | KEYCODE_S = 47 49 | KEYCODE_T = 48 50 | KEYCODE_U = 49 51 | KEYCODE_V = 50 52 | KEYCODE_W = 51 53 | KEYCODE_X = 52 54 | KEYCODE_Y = 53 55 | KEYCODE_Z = 54 56 | KEYCODE_COMMA = 55 57 | KEYCODE_PERIOD = 56 58 | KEYCODE_ALT_LEFT = 57 59 | KEYCODE_ALT_RIGHT = 58 60 | KEYCODE_SHIFT_LEFT = 59 61 | KEYCODE_SHIFT_RIGHT = 60 62 | KEYCODE_TAB = 61 63 | KEYCODE_SPACE = 62 64 | KEYCODE_SYM = 63 65 | KEYCODE_EXPLORER = 64 66 | KEYCODE_ENVELOPE = 65 67 | KEYCODE_ENTER = 66 68 | KEYCODE_DEL = 67 69 | KEYCODE_GRAVE = 68 70 | KEYCODE_MINUS = 69 71 | KEYCODE_EQUALS = 70 72 | KEYCODE_LEFT_BRACKET = 71 73 | KEYCODE_RIGHT_BRACKET = 72 74 | KEYCODE_BACKSLASH = 73 75 | KEYCODE_SEMICOLON = 74 76 | KEYCODE_APOSTROPHE = 75 77 | KEYCODE_SLASH = 76 78 | KEYCODE_AT = 77 79 | KEYCODE_NUM = 78 80 | KEYCODE_HEADSETHOOK = 79 81 | KEYCODE_FOCUS = 80 82 | KEYCODE_PLUS = 81 83 | KEYCODE_MENU = 82 84 | KEYCODE_NOTIFICATION = 83 85 | KEYCODE_SEARCH = 84 86 | KEYCODE_MEDIA_PLAY_PAUSE = 85 87 | KEYCODE_MEDIA_STOP = 86 88 | KEYCODE_MEDIA_NEXT = 87 89 | KEYCODE_MEDIA_PREVIOUS = 88 90 | KEYCODE_MEDIA_REWIND = 89 91 | KEYCODE_MEDIA_FAST_FORWARD = 90 92 | KEYCODE_MUTE = 91 93 | KEYCODE_PAGE_UP = 92 94 | KEYCODE_PAGE_DOWN = 93 95 | KEYCODE_PICTSYMBOLS = 94 96 | KEYCODE_SWITCH_CHARSET = 95 97 | KEYCODE_BUTTON_A = 96 98 | KEYCODE_BUTTON_B = 97 99 | KEYCODE_BUTTON_C = 98 100 | KEYCODE_BUTTON_X = 99 101 | KEYCODE_BUTTON_Y = 100 102 | KEYCODE_BUTTON_Z = 101 103 | KEYCODE_BUTTON_L1 = 102 104 | KEYCODE_BUTTON_R1 = 103 105 | KEYCODE_BUTTON_L2 = 104 106 | KEYCODE_BUTTON_R2 = 105 107 | KEYCODE_BUTTON_THUMBL = 106 108 | KEYCODE_BUTTON_THUMBR = 107 109 | KEYCODE_BUTTON_START = 108 110 | KEYCODE_BUTTON_SELECT = 109 111 | KEYCODE_BUTTON_MODE = 110 112 | KEYCODE_ESCAPE = 111 113 | KEYCODE_FORWARD_DEL = 112 114 | KEYCODE_CTRL_LEFT = 113 115 | KEYCODE_CTRL_RIGHT = 114 116 | KEYCODE_CAPS_LOCK = 115 117 | KEYCODE_SCROLL_LOCK = 116 118 | KEYCODE_META_LEFT = 117 119 | KEYCODE_META_RIGHT = 118 120 | KEYCODE_FUNCTION = 119 121 | KEYCODE_SYSRQ = 120 122 | KEYCODE_BREAK = 121 123 | KEYCODE_MOVE_HOME = 122 124 | KEYCODE_MOVE_END = 123 125 | KEYCODE_INSERT = 124 126 | KEYCODE_FORWARD = 125 127 | KEYCODE_MEDIA_PLAY = 126 128 | KEYCODE_MEDIA_PAUSE = 127 129 | KEYCODE_MEDIA_CLOSE = 128 130 | KEYCODE_MEDIA_EJECT = 129 131 | KEYCODE_MEDIA_RECORD = 130 132 | KEYCODE_F1 = 131 133 | KEYCODE_F2 = 132 134 | KEYCODE_F3 = 133 135 | KEYCODE_F4 = 134 136 | KEYCODE_F5 = 135 137 | KEYCODE_F6 = 136 138 | KEYCODE_F7 = 137 139 | KEYCODE_F8 = 138 140 | KEYCODE_F9 = 139 141 | KEYCODE_F10 = 140 142 | KEYCODE_F11 = 141 143 | KEYCODE_F12 = 142 144 | KEYCODE_NUM_LOCK = 143 145 | KEYCODE_NUMPAD_0 = 144 146 | KEYCODE_NUMPAD_1 = 145 147 | KEYCODE_NUMPAD_2 = 146 148 | KEYCODE_NUMPAD_3 = 147 149 | KEYCODE_NUMPAD_4 = 148 150 | KEYCODE_NUMPAD_5 = 149 151 | KEYCODE_NUMPAD_6 = 150 152 | KEYCODE_NUMPAD_7 = 151 153 | KEYCODE_NUMPAD_8 = 152 154 | KEYCODE_NUMPAD_9 = 153 155 | KEYCODE_NUMPAD_DIVIDE = 154 156 | KEYCODE_NUMPAD_MULTIPLY = 155 157 | KEYCODE_NUMPAD_SUBTRACT = 156 158 | KEYCODE_NUMPAD_ADD = 157 159 | KEYCODE_NUMPAD_DOT = 158 160 | KEYCODE_NUMPAD_COMMA = 159 161 | KEYCODE_NUMPAD_ENTER = 160 162 | KEYCODE_NUMPAD_EQUALS = 161 163 | KEYCODE_NUMPAD_LEFT_PAREN = 162 164 | KEYCODE_NUMPAD_RIGHT_PAREN = 163 165 | KEYCODE_VOLUME_MUTE = 164 166 | KEYCODE_INFO = 165 167 | KEYCODE_CHANNEL_UP = 166 168 | KEYCODE_CHANNEL_DOWN = 167 169 | KEYCODE_ZOOM_IN = 168 170 | KEYCODE_ZOOM_OUT = 169 171 | KEYCODE_TV = 170 172 | KEYCODE_WINDOW = 171 173 | KEYCODE_GUIDE = 172 174 | KEYCODE_DVR = 173 175 | KEYCODE_BOOKMARK = 174 176 | KEYCODE_CAPTIONS = 175 177 | KEYCODE_SETTINGS = 176 178 | KEYCODE_TV_POWER = 177 179 | KEYCODE_TV_INPUT = 178 180 | KEYCODE_STB_POWER = 179 181 | KEYCODE_STB_INPUT = 180 182 | KEYCODE_AVR_POWER = 181 183 | KEYCODE_AVR_INPUT = 182 184 | KEYCODE_PROG_RED = 183 185 | KEYCODE_PROG_GREEN = 184 186 | KEYCODE_PROG_YELLOW = 185 187 | KEYCODE_PROG_BLUE = 186 188 | KEYCODE_APP_SWITCH = 187 189 | KEYCODE_BUTTON_1 = 188 190 | KEYCODE_BUTTON_2 = 189 191 | KEYCODE_BUTTON_3 = 190 192 | KEYCODE_BUTTON_4 = 191 193 | KEYCODE_BUTTON_5 = 192 194 | KEYCODE_BUTTON_6 = 193 195 | KEYCODE_BUTTON_7 = 194 196 | KEYCODE_BUTTON_8 = 195 197 | KEYCODE_BUTTON_9 = 196 198 | KEYCODE_BUTTON_10 = 197 199 | KEYCODE_BUTTON_11 = 198 200 | KEYCODE_BUTTON_12 = 199 201 | KEYCODE_BUTTON_13 = 200 202 | KEYCODE_BUTTON_14 = 201 203 | KEYCODE_BUTTON_15 = 202 204 | KEYCODE_BUTTON_16 = 203 205 | KEYCODE_LANGUAGE_SWITCH = 204 206 | KEYCODE_MANNER_MODE = 205 207 | KEYCODE_3D_MODE = 206 208 | KEYCODE_CONTACTS = 207 209 | KEYCODE_CALENDAR = 208 210 | KEYCODE_MUSIC = 209 211 | KEYCODE_CALCULATOR = 210 212 | KEYCODE_ZENKAKU_HANKAKU = 211 213 | KEYCODE_EISU = 212 214 | KEYCODE_MUHENKAN = 213 215 | KEYCODE_HENKAN = 214 216 | KEYCODE_KATAKANA_HIRAGANA = 215 217 | KEYCODE_YEN = 216 218 | KEYCODE_RO = 217 219 | KEYCODE_KANA = 218 220 | KEYCODE_ASSIST = 219 221 | KEYCODE_BRIGHTNESS_DOWN = 220 222 | KEYCODE_BRIGHTNESS_UP = 221 223 | KEYCODE_MEDIA_AUDIO_TRACK = 222 224 | -------------------------------------------------------------------------------- /ppadb/plugins/__init__.py: -------------------------------------------------------------------------------- 1 | class Plugin: 2 | def shell(self, cmd, handler=None, timeout=None): 3 | pass 4 | -------------------------------------------------------------------------------- /ppadb/plugins/client/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swind/pure-python-adb/b136202b04660db57b49418514dd9eddc2ecb365/ppadb/plugins/client/__init__.py -------------------------------------------------------------------------------- /ppadb/plugins/device/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swind/pure-python-adb/b136202b04660db57b49418514dd9eddc2ecb365/ppadb/plugins/device/__init__.py -------------------------------------------------------------------------------- /ppadb/plugins/device/batterystats.py: -------------------------------------------------------------------------------- 1 | from ppadb.plugins.device import batterystats_section as section_module 2 | from ppadb.plugins import Plugin 3 | from ppadb.utils.logger import AdbLogging 4 | 5 | logger = AdbLogging.get_logger(__name__) 6 | 7 | 8 | class BatteryStats(Plugin): 9 | def get_battery_level(self): 10 | battery = self.shell("dumpsys battery") 11 | 12 | for line in battery.split('\n'): 13 | tokens = line.split(":") 14 | if tokens[0].strip() == "level" and len(tokens) == 2: 15 | return int(tokens[1]) 16 | 17 | return None 18 | 19 | 20 | def get_batterystats(self): 21 | result = self.shell("dumpsys batterystats -c") 22 | sections = {} 23 | for line in result.split("\n"): 24 | if not line.strip(): 25 | continue 26 | 27 | tokens = line.split(",", 4) 28 | if len(tokens) < 5: 29 | continue 30 | 31 | dummy, uid, mode, id, remaining_fields = tokens 32 | print(dummy, uid, mode, id, remaining_fields) 33 | SectionClass = section_module.get_section(id) 34 | if not SectionClass: 35 | logger.error("Unknown section {} in batterystats".format(id)) 36 | continue 37 | 38 | if id not in sections: 39 | sections[id] = [] 40 | 41 | sections[id].append(SectionClass(*remaining_fields.split(","))) 42 | 43 | return sections 44 | -------------------------------------------------------------------------------- /ppadb/plugins/device/batterystats_section.py: -------------------------------------------------------------------------------- 1 | 2 | class Version: 3 | def __init__(self, checkin_version,parcel_version,start_platform_version,end_platform_version): 4 | self.id = "vers" 5 | self.checkin_version = checkin_version 6 | self.parcel_version = parcel_version 7 | self.start_platform_version = start_platform_version 8 | self.end_platform_version = end_platform_version 9 | 10 | 11 | class UID: 12 | def __init__(self, uid,package_name): 13 | self.id = "uid" 14 | self.uid = uid 15 | self.package_name = package_name 16 | 17 | 18 | class APK: 19 | def __init__(self, wakeups,apk,service,start_time,starts,launches): 20 | self.id = "apk" 21 | self.wakeups = wakeups 22 | self.apk = apk 23 | self.service = service 24 | self.start_time = start_time 25 | self.starts = starts 26 | self.launches = launches 27 | 28 | 29 | class Process: 30 | def __init__(self, process,user,system,foreground,starts): 31 | self.id = "pr" 32 | self.process = process 33 | self.user = user 34 | self.system = system 35 | self.foreground = foreground 36 | self.starts = starts 37 | 38 | 39 | class Sensor: 40 | def __init__(self, sensor_number,time,count): 41 | self.id = "sr" 42 | self.sensor_number = sensor_number 43 | self.time = time 44 | self.count = count 45 | 46 | 47 | class Vibrator: 48 | def __init__(self, time,count): 49 | self.id = "vib" 50 | self.time = time 51 | self.count = count 52 | 53 | 54 | class Foreground: 55 | def __init__(self, time,count): 56 | self.id = "fg" 57 | self.time = time 58 | self.count = count 59 | 60 | 61 | class StateTime: 62 | def __init__(self, foreground,active,running): 63 | self.id = "st" 64 | self.foreground = foreground 65 | self.active = active 66 | self.running = running 67 | 68 | 69 | class Wakelock: 70 | def __init__(self, wake_lock,full_time,f,full_count,partial_time,p,partial_count,window_time,w,window_count): 71 | self.id = "wl" 72 | self.wake_lock = wake_lock 73 | self.full_time = full_time 74 | self.f = f 75 | self.full_count = full_count 76 | self.partial_time = partial_time 77 | self.p = p 78 | self.partial_count = partial_count 79 | self.window_time = window_time 80 | self.w = w 81 | self.window_count = window_count 82 | 83 | 84 | class Sync: 85 | def __init__(self, sync,time,count): 86 | self.id = "sy" 87 | self.sync = sync 88 | self.time = time 89 | self.count = count 90 | 91 | 92 | class Job: 93 | def __init__(self, job,time,count): 94 | self.id = "jb" 95 | self.job = job 96 | self.time = time 97 | self.count = count 98 | 99 | 100 | class KernelWakeLock: 101 | def __init__(self, kernel_wake_lock,time,count): 102 | self.id = "kwl" 103 | self.kernel_wake_lock = kernel_wake_lock 104 | self.time = time 105 | self.count = count 106 | 107 | 108 | class WakeupReason: 109 | def __init__(self, wakeup_reason,time,count): 110 | self.id = "wr" 111 | self.wakeup_reason = wakeup_reason 112 | self.time = time 113 | self.count = count 114 | 115 | 116 | class Network: 117 | def __init__(self, mobile_bytes_rx,mobile_bytes_tx,wifi_bytes_rx,wifi_bytes_tx,mobile_packets_rx,mobile_packets_tx,wifi_packets_rx,wifi_packets_tx,mobile_active_time,mobile_active_count): 118 | self.id = "nt" 119 | self.mobile_bytes_rx = mobile_bytes_rx 120 | self.mobile_bytes_tx = mobile_bytes_tx 121 | self.wifi_bytes_rx = wifi_bytes_rx 122 | self.wifi_bytes_tx = wifi_bytes_tx 123 | self.mobile_packets_rx = mobile_packets_rx 124 | self.mobile_packets_tx = mobile_packets_tx 125 | self.wifi_packets_rx = wifi_packets_rx 126 | self.wifi_packets_tx = wifi_packets_tx 127 | self.mobile_active_time = mobile_active_time 128 | self.mobile_active_count = mobile_active_count 129 | 130 | 131 | class UserActivity: 132 | def __init__(self, other,button,touch): 133 | self.id = "ua" 134 | self.other = other 135 | self.button = button 136 | self.touch = touch 137 | 138 | 139 | class Battery: 140 | def __init__(self, start_count,battery_realtime,battery_uptime,total_realtime,total_uptime,start_clock_time,battery_screen_off_realtime,battery_screen_off_uptime): 141 | self.id = "bt" 142 | self.start_count = start_count 143 | self.battery_realtime = battery_realtime 144 | self.battery_uptime = battery_uptime 145 | self.total_realtime = total_realtime 146 | self.total_uptime = total_uptime 147 | self.start_clock_time = start_clock_time 148 | self.battery_screen_off_realtime = battery_screen_off_realtime 149 | self.battery_screen_off_uptime = battery_screen_off_uptime 150 | 151 | 152 | class BatteryDischarge: 153 | def __init__(self, low,high,screen_on,screen_off): 154 | self.id = "dc" 155 | self.low = low 156 | self.high = high 157 | self.screen_on = screen_on 158 | self.screen_off = screen_off 159 | 160 | 161 | class BatteryLevel: 162 | def __init__(self, start_level,current_level): 163 | self.id = "lv" 164 | self.start_level = start_level 165 | self.current_level = current_level 166 | 167 | 168 | class WiFi: 169 | def __init__(self, full_wifi_lock_on_time,wifi_scan_time,wifi_running_time,wifi_scan_count,wifi_idle_time,wifi_receive_time,wifi_transmit_time): 170 | self.id = "wfl" 171 | self.full_wifi_lock_on_time = full_wifi_lock_on_time 172 | self.wifi_scan_time = wifi_scan_time 173 | self.wifi_running_time = wifi_running_time 174 | self.wifi_scan_count = wifi_scan_count 175 | self.wifi_idle_time = wifi_idle_time 176 | self.wifi_receive_time = wifi_receive_time 177 | self.wifi_transmit_time = wifi_transmit_time 178 | 179 | 180 | class GlobalWiFi: 181 | def __init__(self, wifi_on_time,wifi_running_time,wifi_idle_time,wifi_receive_time,wifi_transmit_time,wifi_power_mah): 182 | self.id = "gwfl" 183 | self.wifi_on_time = wifi_on_time 184 | self.wifi_running_time = wifi_running_time 185 | self.wifi_idle_time = wifi_idle_time 186 | self.wifi_receive_time = wifi_receive_time 187 | self.wifi_transmit_time = wifi_transmit_time 188 | self.wifi_power_mah = wifi_power_mah 189 | 190 | 191 | class GlobalBluetooth: 192 | def __init__(self, bt_idle_time,bt_receive_time,bt_transmit_time,bt_power_mah): 193 | self.id = "gble" 194 | self.bt_idle_time = bt_idle_time 195 | self.bt_receive_time = bt_receive_time 196 | self.bt_transmit_time = bt_transmit_time 197 | self.bt_power_mah = bt_power_mah 198 | 199 | 200 | class Misc: 201 | def __init__(self, screen_on_time,phone_on_time,full_wakelock_time_total,partial_wakelock_time_total,mobile_radio_active_time,mobile_radio_active_adjusted_time,interactive_time,power_save_mode_enabled_time,connectivity_changes,device_idle_mode_enabled_time,device_idle_mode_enabled_count,device_idling_time,device_idling_count,mobile_radio_active_count,mobile_radio_active_unknown_time): 202 | self.id = "m" 203 | self.screen_on_time = screen_on_time 204 | self.phone_on_time = phone_on_time 205 | self.full_wakelock_time_total = full_wakelock_time_total 206 | self.partial_wakelock_time_total = partial_wakelock_time_total 207 | self.mobile_radio_active_time = mobile_radio_active_time 208 | self.mobile_radio_active_adjusted_time = mobile_radio_active_adjusted_time 209 | self.interactive_time = interactive_time 210 | self.power_save_mode_enabled_time = power_save_mode_enabled_time 211 | self.connectivity_changes = connectivity_changes 212 | self.device_idle_mode_enabled_time = device_idle_mode_enabled_time 213 | self.device_idle_mode_enabled_count = device_idle_mode_enabled_count 214 | self.device_idling_time = device_idling_time 215 | self.device_idling_count = device_idling_count 216 | self.mobile_radio_active_count = mobile_radio_active_count 217 | self.mobile_radio_active_unknown_time = mobile_radio_active_unknown_time 218 | 219 | 220 | class GlobalNetwork: 221 | def __init__(self, mobile_rx_total_bytes,mobile_tx_total_bytes,wifi_rx_total_bytes,wifi_tx_total_bytes,mobile_rx_total_packets,mobile_tx_total_packets,wifi_rx_total_packets,wifi_tx_total_packets): 222 | self.id = "gn" 223 | self.mobile_rx_total_bytes = mobile_rx_total_bytes 224 | self.mobile_tx_total_bytes = mobile_tx_total_bytes 225 | self.wifi_rx_total_bytes = wifi_rx_total_bytes 226 | self.wifi_tx_total_bytes = wifi_tx_total_bytes 227 | self.mobile_rx_total_packets = mobile_rx_total_packets 228 | self.mobile_tx_total_packets = mobile_tx_total_packets 229 | self.wifi_rx_total_packets = wifi_rx_total_packets 230 | self.wifi_tx_total_packets = wifi_tx_total_packets 231 | 232 | 233 | class ScreenBrightness: 234 | def __init__(self, dark,dim,medium,light,bright): 235 | self.id = "br" 236 | self.dark = dark 237 | self.dim = dim 238 | self.medium = medium 239 | self.light = light 240 | self.bright = bright 241 | 242 | 243 | class SignalScanningTime: 244 | def __init__(self, signal_scanning_time): 245 | self.id = "sst" 246 | self.signal_scanning_time = signal_scanning_time 247 | 248 | 249 | class SignalStrengthTime: 250 | def __init__(self, none,poor,moderate,good,great): 251 | self.id = "sgt" 252 | self.none = none 253 | self.poor = poor 254 | self.moderate = moderate 255 | self.good = good 256 | self.great = great 257 | 258 | 259 | class SignalStrengthCount: 260 | def __init__(self, none,poor,moderate,good,great): 261 | self.id = "sgc" 262 | self.none = none 263 | self.poor = poor 264 | self.moderate = moderate 265 | self.good = good 266 | self.great = great 267 | 268 | 269 | class DataConnectionTime: 270 | def __init__(self, none,gprs,edge,umts,cdma,evdo_0,evdo_a,_1xrtt,hsdpa,hsupa,hspa,iden,evdo_b,lte,ehrpd,hspap,other): 271 | self.id = "dct" 272 | self.none = none 273 | self.gprs = gprs 274 | self.edge = edge 275 | self.umts = umts 276 | self.cdma = cdma 277 | self.evdo_0 = evdo_0 278 | self.evdo_a = evdo_a 279 | self._1xrtt = _1xrtt 280 | self.hsdpa = hsdpa 281 | self.hsupa = hsupa 282 | self.hspa = hspa 283 | self.iden = iden 284 | self.evdo_b = evdo_b 285 | self.lte = lte 286 | self.ehrpd = ehrpd 287 | self.hspap = hspap 288 | self.other = other 289 | 290 | 291 | class DataConnectionCount: 292 | def __init__(self, none,gprs,edge,umts,cdma,evdo_0,evdo_a,_1xrtt,hsdpa,hsupa,hspa,iden,evdo_b,lte,ehrpd,hspap,other): 293 | self.id = "dcc" 294 | self.none = none 295 | self.gprs = gprs 296 | self.edge = edge 297 | self.umts = umts 298 | self.cdma = cdma 299 | self.evdo_0 = evdo_0 300 | self.evdo_a = evdo_a 301 | self._1xrtt = _1xrtt 302 | self.hsdpa = hsdpa 303 | self.hsupa = hsupa 304 | self.hspa = hspa 305 | self.iden = iden 306 | self.evdo_b = evdo_b 307 | self.lte = lte 308 | self.ehrpd = ehrpd 309 | self.hspap = hspap 310 | self.other = other 311 | 312 | 313 | class WiFiStateTime: 314 | def __init__(self, off,off_scanning,on_no_networks,on_disconnected,on_connected_sta,on_connected_p2p,on_connected_sta_p2p,soft_ap): 315 | self.id = "wst" 316 | self.off = off 317 | self.off_scanning = off_scanning 318 | self.on_no_networks = on_no_networks 319 | self.on_disconnected = on_disconnected 320 | self.on_connected_sta = on_connected_sta 321 | self.on_connected_p2p = on_connected_p2p 322 | self.on_connected_sta_p2p = on_connected_sta_p2p 323 | self.soft_ap = soft_ap 324 | 325 | 326 | class WiFiStateCount: 327 | def __init__(self, off,off_scanning,on_no_networks,on_disconnected,on_connected_sta,on_connected_p2p,on_connected_sta_p2p,soft_ap): 328 | self.id = "wsc" 329 | self.off = off 330 | self.off_scanning = off_scanning 331 | self.on_no_networks = on_no_networks 332 | self.on_disconnected = on_disconnected 333 | self.on_connected_sta = on_connected_sta 334 | self.on_connected_p2p = on_connected_p2p 335 | self.on_connected_sta_p2p = on_connected_sta_p2p 336 | self.soft_ap = soft_ap 337 | 338 | 339 | class WiFiSupplicantStateTime: 340 | def __init__(self, invalid,disconnected,interface_disabled,inactive,scanning,authenticating,associating,associated,four_way_handshake,group_handshake,completed,dormant,uninitialized): 341 | self.id = "wsst" 342 | self.invalid = invalid 343 | self.disconnected = disconnected 344 | self.interface_disabled = interface_disabled 345 | self.inactive = inactive 346 | self.scanning = scanning 347 | self.authenticating = authenticating 348 | self.associating = associating 349 | self.associated = associated 350 | self.four_way_handshake = four_way_handshake 351 | self.group_handshake = group_handshake 352 | self.completed = completed 353 | self.dormant = dormant 354 | self.uninitialized = uninitialized 355 | 356 | 357 | class WiFiSupplicantStateCount: 358 | def __init__(self, invalid,disconnected,interface_disabled,inactive,scanning,authenticating,associating,associated,four_way_handshake,group_handshake,completed,dormant,uninitialized): 359 | self.id = "wssc" 360 | self.invalid = invalid 361 | self.disconnected = disconnected 362 | self.interface_disabled = interface_disabled 363 | self.inactive = inactive 364 | self.scanning = scanning 365 | self.authenticating = authenticating 366 | self.associating = associating 367 | self.associated = associated 368 | self.four_way_handshake = four_way_handshake 369 | self.group_handshake = group_handshake 370 | self.completed = completed 371 | self.dormant = dormant 372 | self.uninitialized = uninitialized 373 | 374 | 375 | class WiFiSignalStrengthTime: 376 | def __init__(self, none,poor,moderate,good,great): 377 | self.id = "wsgt" 378 | self.none = none 379 | self.poor = poor 380 | self.moderate = moderate 381 | self.good = good 382 | self.great = great 383 | 384 | 385 | class WiFiSignalStrengthCount: 386 | def __init__(self, none,poor,moderate,good,great): 387 | self.id = "wsgc" 388 | self.none = none 389 | self.poor = poor 390 | self.moderate = moderate 391 | self.good = good 392 | self.great = great 393 | 394 | 395 | class BluetoothStateTime: 396 | def __init__(self, inactive,low,med,high): 397 | self.id = "bst" 398 | self.inactive = inactive 399 | self.low = low 400 | self.med = med 401 | self.high = high 402 | 403 | 404 | class BluetoothStateCount: 405 | def __init__(self, inactive,low,med,high): 406 | self.id = "bsc" 407 | self.inactive = inactive 408 | self.low = low 409 | self.med = med 410 | self.high = high 411 | 412 | mapping={ 413 | 'vers':Version, 414 | 'uid':UID, 415 | 'apk':APK, 416 | 'pr':Process, 417 | 'sr':Sensor, 418 | 'vib':Vibrator, 419 | 'fg':Foreground, 420 | 'st':StateTime, 421 | 'wl':Wakelock, 422 | 'sy':Sync, 423 | 'jb':Job, 424 | 'kwl':KernelWakeLock, 425 | 'wr':WakeupReason, 426 | 'nt':Network, 427 | 'ua':UserActivity, 428 | 'bt':Battery, 429 | 'dc':BatteryDischarge, 430 | 'lv':BatteryLevel, 431 | 'wfl':WiFi, 432 | 'gwfl':GlobalWiFi, 433 | 'gble':GlobalBluetooth, 434 | 'm':Misc, 435 | 'gn':GlobalNetwork, 436 | 'br':ScreenBrightness, 437 | 'sst':SignalScanningTime, 438 | 'sgt':SignalStrengthTime, 439 | 'sgc':SignalStrengthCount, 440 | 'dct':DataConnectionTime, 441 | 'dcc':DataConnectionCount, 442 | 'wst':WiFiStateTime, 443 | 'wsc':WiFiStateCount, 444 | 'wsst':WiFiSupplicantStateTime, 445 | 'wssc':WiFiSupplicantStateCount, 446 | 'wsgt':WiFiSignalStrengthTime, 447 | 'wsgc':WiFiSignalStrengthCount, 448 | 'bst':BluetoothStateTime, 449 | 'bsc':BluetoothStateCount 450 | } 451 | 452 | def get_section(name): 453 | return mapping.get(name) 454 | -------------------------------------------------------------------------------- /ppadb/plugins/device/cpustat.py: -------------------------------------------------------------------------------- 1 | import re 2 | import time 3 | 4 | from ppadb.plugins import Plugin 5 | from ppadb.utils.logger import AdbLogging 6 | 7 | logger = AdbLogging.get_logger(__name__) 8 | 9 | class TotalCPUStat: 10 | def __init__(self, user, nice, system, idle, iowait, irq, softirq, stealstolen, guest, guest_nice): 11 | self.user = user 12 | self.nice = nice 13 | self.system = system 14 | self.idle = idle 15 | self.iowait = iowait 16 | self.irq = irq 17 | self.softirq = softirq 18 | self.stealstolen = stealstolen 19 | self.guest = guest 20 | self.guest_nice = guest_nice 21 | 22 | def total(self): 23 | return self.user + self.nice + self.system + self.idle + self.iowait + self.irq + self.softirq + self.stealstolen + self.guest + self.guest_nice 24 | 25 | def __add__(self, other): 26 | summary = TotalCPUStat(0, 0, 0, 0, 0, 0, 0, 0, 0, 0) 27 | 28 | summary.user = self.user + other.user 29 | summary.nice = self.nice + other.nice 30 | summary.system = self.system + other.system 31 | summary.idle = self.idle + other.idle 32 | summary.iowait = self.iowait + other.iowait 33 | summary.irq = self.irq + other.irq 34 | summary.softirq = self.softirq + other.softirq 35 | summary.stealstolen = self.stealstolen + other.stealstolen 36 | summary.guest = self.guest + other.guest 37 | summary.guest_nice = self.guest_nice + other.guest_nice 38 | 39 | return summary 40 | 41 | def __sub__(self, other): 42 | result = TotalCPUStat(0, 0, 0, 0, 0, 0, 0, 0, 0, 0) 43 | 44 | result.user = self.user - other.user 45 | result.nice = self.nice - other.nice 46 | result.system = self.system - other.system 47 | result.idle = self.idle - other.idle 48 | result.iowait = self.iowait - other.iowait 49 | result.irq = self.irq - other.irq 50 | result.softirq = self.softirq - other.softirq 51 | result.stealstolen = self.stealstolen - other.stealstolen 52 | result.guest = self.guest - other.guest 53 | result.guest_nice = self.guest_nice - other.guest_nice 54 | 55 | return result 56 | 57 | def __str__(self): 58 | attrs = vars(self) 59 | return ', '.join("%s: %s" % item for item in attrs.items()) 60 | 61 | 62 | class ProcessCPUStat: 63 | def __init__(self, name, utime, stime): 64 | self.name = name 65 | self.utime = utime 66 | self.stime = stime 67 | 68 | def __add__(self, other): 69 | summary = ProcessCPUStat(self.name, 0, 0) 70 | summary.utime = self.utime + other.utime 71 | summary.stime = self.stime + other.stime 72 | return summary 73 | 74 | def __sub__(self, other): 75 | result = ProcessCPUStat(self.name, 0, 0) 76 | result.utime = self.utime - other.utime 77 | result.stime = self.stime - other.stime 78 | return result 79 | 80 | def __str__(self): 81 | attrs = vars(self) 82 | return ', '.join("%s: %s" % item for item in attrs.items()) 83 | 84 | def total(self): 85 | return self.utime + self.stime 86 | 87 | 88 | class CPUStat(Plugin): 89 | total_cpu_pattern = re.compile( 90 | "cpu\s+([\d]+)\s([\d]+)\s([\d]+)\s([\d]+)\s([\d]+)\s([\d]+)\s([\d]+)\s([\d]+)\s([\d]+)\s([\d]+)\s") 91 | 92 | def cpu_times(self): 93 | return self.get_total_cpu() 94 | 95 | def cpu_percent(self, interval=1): 96 | cpu_times_start = self.cpu_times() 97 | time.sleep(interval) 98 | cpu_times_end = self.cpu_times() 99 | 100 | diff = cpu_times_end - cpu_times_start 101 | return round(((diff.user + diff.system) / diff.total()) * 100, 2) 102 | 103 | def cpu_count(self): 104 | result = self.shell('ls /sys/devices/system/cpu') 105 | match = re.findall(r'cpu[0-9+]', result) 106 | return len(match) 107 | 108 | def get_total_cpu(self): 109 | result = self.shell('cat /proc/stat') 110 | match = self.total_cpu_pattern.search(result) 111 | if not match and len(match.groups()) != 10: 112 | logger.error("Can't get the total cpu usage from /proc/stat") 113 | return None 114 | 115 | return TotalCPUStat(*map(lambda x: int(x), match.groups())) 116 | 117 | def get_pid_cpu(self, pid): 118 | result = self.shell('cat /proc/{}/stat'.format(pid)).strip() 119 | 120 | if "No such file or directory" in result: 121 | return ProcessCPUStat("", 0, 0) 122 | else: 123 | items = result.split() 124 | return ProcessCPUStat(items[1], int(items[13]), int(items[14])) 125 | 126 | def get_all_thread_cpu(self, pid): 127 | result = self.shell("ls /proc/{}/task".format(pid)) 128 | tids = list(map(lambda line: line.strip(), result.split("\n"))) 129 | 130 | thread_result = {} 131 | for tid in tids: 132 | result = self.shell("cat /proc/{}/task/{}/stat".format(pid, tid)) 133 | 134 | if "No such file or directory" not in result: 135 | items = result.split() 136 | thread_result[tid] = ProcessCPUStat(items[1], int(items[13]), int(items[14])) 137 | 138 | return thread_result 139 | -------------------------------------------------------------------------------- /ppadb/plugins/device/input.py: -------------------------------------------------------------------------------- 1 | from ppadb.plugins import Plugin 2 | 3 | 4 | class Source: 5 | KEYBOARD = 'keyboard' 6 | MOUSE = 'mouse' 7 | JOYSTICK = 'joystick' 8 | TOUCHNAVIGATION = 'touchnavigation' 9 | TOUCHPAD = 'touchpad' 10 | TRACKBALL = 'trackball' 11 | DPAD = 'dpad' 12 | STYLUS = 'stylus' 13 | GAMEPAD = 'gamepad' 14 | touchscreen = 'touchscreen' 15 | 16 | 17 | class Input(Plugin): 18 | def input_text(self, string): 19 | return self.shell('input text "{}"'.format(string)) 20 | 21 | def input_keyevent(self, keycode, longpress=False): 22 | cmd = 'input keyevent {}'.format(keycode) 23 | if longpress: 24 | cmd += " --longpress" 25 | return self.shell(cmd) 26 | 27 | def input_tap(self, x, y): 28 | return self.shell("input tap {} {}".format(x, y)) 29 | 30 | def input_swipe(self, start_x, start_y, end_x, end_y, duration): 31 | return self.shell("input swipe {} {} {} {} {}".format( 32 | start_x, 33 | start_y, 34 | end_x, 35 | end_y, 36 | duration 37 | )) 38 | 39 | def input_press(self): 40 | return self.shell("input press") 41 | 42 | def input_roll(self, dx, dy): 43 | return self.roll("roll {} {}".format(dx, dy)) 44 | -------------------------------------------------------------------------------- /ppadb/plugins/device/traffic.py: -------------------------------------------------------------------------------- 1 | import re 2 | from collections import namedtuple 3 | 4 | from ppadb.plugins import Plugin 5 | 6 | State = namedtuple("TrafficState", [ 7 | 'idx', 8 | 'iface', 9 | 'acct_tag_hex', 10 | 'uid_tag_int', 11 | 'cnt_set', 12 | 'rx_bytes', 13 | 'rx_packets', 14 | 'tx_bytes', 15 | 'tx_packets', 16 | 'rx_tcp_bytes', 17 | 'rx_tcp_packets', 18 | 'rx_udp_bytes', 19 | 'rx_udp_packets', 20 | 'rx_other_bytes', 21 | 'rx_other_packets', 22 | 'tx_tcp_bytes', 23 | 'tx_tcp_packets', 24 | 'tx_udp_bytes', 25 | 'tx_udp_packets', 26 | 'tx_other_bytes', 27 | 'tx_other_packets', 28 | ]) 29 | 30 | 31 | class Traffic(Plugin): 32 | def get_traffic(self, package_name): 33 | cmd = 'dumpsys package {} | grep userId'.format(package_name) 34 | result = self.shell(cmd).strip() 35 | 36 | pattern = "userId=([\d]+)" 37 | 38 | if result: 39 | match = re.search(pattern, result) 40 | uid = match.group(1) 41 | else: 42 | # This package is not existing 43 | return None 44 | 45 | cmd = 'cat /proc/net/xt_qtaguid/stats | grep {}'.format(uid) 46 | result = self.shell(cmd) 47 | 48 | def convert(token): 49 | if token.isdigit(): 50 | return int(token) 51 | else: 52 | return token 53 | 54 | states = [] 55 | if result: 56 | for line in result.strip().split('\n'): 57 | values = map(convert, line.split()) 58 | states.append(State(*values)) 59 | 60 | return states 61 | else: 62 | return None 63 | -------------------------------------------------------------------------------- /ppadb/plugins/device/utils.py: -------------------------------------------------------------------------------- 1 | import re 2 | from ppadb.plugins import Plugin 3 | 4 | 5 | class Activity: 6 | def __init__(self, package, activity, pid): 7 | self.package = package 8 | self.activity = activity 9 | self.pid = pid 10 | 11 | def __str__(self): 12 | return "{}/{} - {}".format(self.package, self.activity, self.pid) 13 | 14 | 15 | class MemInfo: 16 | def __init__(self, pss, private_dirty, private_clean, swapped_dirty, heap_size, heap_alloc, heap_free): 17 | self.pss = int(pss) 18 | self.private_dirty = int(private_dirty) 19 | self.private_clean = int(private_clean) 20 | self.swapped_dirty = int(swapped_dirty) 21 | self.heap_size = int(heap_size) 22 | self.heap_alloc = int(heap_alloc) 23 | self.heap_free = int(heap_free) 24 | 25 | 26 | class Utils(Plugin): 27 | def get_top_activity(self): 28 | activities = self.get_top_activities() 29 | if activities: 30 | return activities[0] 31 | else: 32 | return None 33 | 34 | def get_top_activities(self): 35 | pattern = "ACTIVITY\s([\w\.]+)/([\w\.]+)\s[\w\d]+\spid=([\d]+)" 36 | cmd = "dumpsys activity top | grep ACTIVITY" 37 | result = self.shell(cmd) 38 | 39 | activities = [] 40 | for line in result.split('\n'): 41 | match = re.search(pattern, line) 42 | if match: 43 | activities.append(Activity(match.group(1), match.group(2), int(match.group(3)))) 44 | 45 | return activities 46 | 47 | def get_meminfo(self, package_name): 48 | total_meminfo_re = re.compile('\s*TOTAL\s*(?P\d+)' 49 | '\s*(?P\d+)' 50 | '\s*(?P\d+)' 51 | '\s*(?P\d+)' 52 | '\s*(?P\d+)' 53 | '\s*(?P\d+)' 54 | '\s*(?P\d+)') 55 | 56 | cmd = 'dumpsys meminfo {}'.format(package_name) 57 | result = self.shell(cmd) 58 | match = total_meminfo_re.search(result, 0) 59 | 60 | if match: 61 | return MemInfo(**match.groupdict()) 62 | else: 63 | return MemInfo(0, 0, 0, 0, 0, 0, 0) 64 | 65 | def get_pid(self, package_name, toybox=False): 66 | # Because the version of `ps` is too much, 67 | # For example, the `ps` of toybox needs `-A` to list all process, but the `ps` of emulator doesn't. 68 | # So we use 'ps' and 'ps -A' to get all process information. 69 | 70 | cmds = ["ps | grep {}", "ps -A | grep {}"] 71 | for cmd in cmds: 72 | result = self.shell(cmd.format(package_name)) 73 | if result: 74 | break 75 | 76 | if result: 77 | return result.split()[1] 78 | else: 79 | return None 80 | 81 | def get_uid(self, package_name): 82 | cmd = 'dumpsys package {} | grep userId'.format(package_name) 83 | result = self.shell(cmd).strip() 84 | 85 | pattern = "userId=([\d]+)" 86 | 87 | if result: 88 | match = re.search(pattern, result) 89 | uid = match.group(1) 90 | return uid 91 | else: 92 | return None 93 | 94 | def get_tids(self, pid): 95 | result = self.shell("ls /proc/{}/task".format(pid)) 96 | return list(map(lambda line: line.strip(), result.split("\n"))) 97 | 98 | def get_package_version_name(self, package_name): 99 | cmd = 'dumpsys package {} | grep versionName'.format(package_name) 100 | result = self.shell(cmd).strip() 101 | 102 | pattern = "versionName=([\d\.]+)" 103 | 104 | if result: 105 | match = re.search(pattern, result) 106 | version = match.group(1) 107 | return version 108 | else: 109 | return None 110 | -------------------------------------------------------------------------------- /ppadb/plugins/device/wm.py: -------------------------------------------------------------------------------- 1 | import re 2 | from collections import namedtuple 3 | 4 | from ppadb.plugins import Plugin 5 | 6 | Size = namedtuple("Size", [ 7 | 'width', 8 | 'height' 9 | ]) 10 | 11 | class WM(Plugin): 12 | SIZE_RE = 'Physical size:\s([\d]+)x([\d]+)' 13 | def wm_size(self): 14 | result = self.shell("wm size") 15 | match = re.search(self.SIZE_RE, result) 16 | 17 | if match: 18 | return Size(int(match.group(1)), int(match.group(2))) 19 | else: 20 | return None 21 | 22 | def wm_density(self): 23 | result = self.shell("wm density | cut -d ' ' -f 3") 24 | if result: 25 | return int(int(result) / 160) 26 | else: 27 | return None 28 | -------------------------------------------------------------------------------- /ppadb/protocol.py: -------------------------------------------------------------------------------- 1 | class Protocol: 2 | OKAY = 'OKAY' 3 | FAIL = 'FAIL' 4 | STAT = 'STAT' 5 | LIST = 'LIST' 6 | DENT = 'DENT' 7 | RECV = 'RECV' 8 | DATA = 'DATA' 9 | DONE = 'DONE' 10 | SEND = 'SEND' 11 | QUIT = 'QUIT' 12 | 13 | @staticmethod 14 | def decode_length(length): 15 | return int(length, 16) 16 | 17 | @staticmethod 18 | def encode_length(length): 19 | return "{0:04X}".format(length) 20 | 21 | @staticmethod 22 | def encode_data(data): 23 | b_data = data.encode('utf-8') 24 | b_length = Protocol.encode_length(len(b_data)).encode('utf-8') 25 | return b"".join([b_length, b_data]) 26 | -------------------------------------------------------------------------------- /ppadb/sync/__init__.py: -------------------------------------------------------------------------------- 1 | import struct 2 | import time 3 | import os 4 | 5 | from ppadb.protocol import Protocol 6 | from ppadb.sync.stats import S_IFREG 7 | 8 | import logging 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | 13 | class Sync: 14 | TEMP_PATH = '/data/local/tmp' 15 | DEFAULT_CHMOD = 0o644 16 | DATA_MAX_LENGTH = 65536 17 | 18 | def __init__(self, connection): 19 | self.connection = connection 20 | 21 | @staticmethod 22 | def temp(path): 23 | return "{}/{}".format(Sync.TEMP_PATH, os.path.basename(path)) 24 | 25 | def push(self, src, dest, mode, progress=None): 26 | """Push from local path |src| to |dest| on device. 27 | 28 | :param progress: callback, called with (filename, total_size, sent_size) 29 | """ 30 | if not os.path.exists(src): 31 | raise FileNotFoundError("Can't find the source file {}".format(src)) 32 | 33 | stat = os.stat(src) 34 | 35 | timestamp = int(stat.st_mtime) 36 | 37 | total_size = os.path.getsize(src) 38 | sent_size = 0 39 | 40 | # SEND 41 | mode = mode | S_IFREG 42 | args = "{dest},{mode}".format( 43 | dest=dest, 44 | mode=mode 45 | ) 46 | self._send_str(Protocol.SEND, args) 47 | 48 | # DATA 49 | with open(src, 'rb') as stream: 50 | while True: 51 | chunk = stream.read(self.DATA_MAX_LENGTH) 52 | if not chunk: 53 | break 54 | 55 | sent_size += len(chunk) 56 | self._send_length(Protocol.DATA, len(chunk)) 57 | self.connection.write(chunk) 58 | 59 | if progress is not None: 60 | progress(src, total_size, sent_size) 61 | 62 | # DONE 63 | self._send_length(Protocol.DONE, timestamp) 64 | self.connection._check_status() 65 | 66 | def pull(self, src, dest): 67 | error = None 68 | 69 | # RECV 70 | self._send_str(Protocol.RECV, src) 71 | 72 | # DATA 73 | with open(dest, 'wb') as stream: 74 | while True: 75 | flag = self.connection.read(4).decode('utf-8') 76 | 77 | if flag == Protocol.DATA: 78 | data = self._read_data() 79 | stream.write(data) 80 | elif flag == Protocol.DONE: 81 | self.connection.read(4) 82 | return 83 | elif flag == Protocol.FAIL: 84 | return self._read_data().decode('utf-8') 85 | 86 | def _integer(self, little_endian): 87 | return struct.unpack("=0.4.0"]}, 34 | keywords="adb", 35 | classifiers=classifiers, 36 | ) 37 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swind/pure-python-adb/b136202b04660db57b49418514dd9eddc2ecb365/test/__init__.py -------------------------------------------------------------------------------- /test/conftest.py: -------------------------------------------------------------------------------- 1 | import time 2 | import telnetlib 3 | 4 | import pytest 5 | 6 | from ppadb.client import Client as AdbClient 7 | from ppadb.device import Device as AdbDevice 8 | import logging 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | adb_host = "emulator" 13 | #adb_host = "127.0.0.1" 14 | #adb_host = "172.20.0.2" 15 | adb_port = 5037 16 | device_serial = "emulator-5554" 17 | emulator_port = 5554 18 | 19 | class EmulatorConsole: 20 | def __init__(self, host, port): 21 | self._port = port 22 | self._telnet = telnetlib.Telnet(host=host, port=port) 23 | print(self._telnet.read_until(b"OK", timeout=5)) 24 | 25 | def send(self, data): 26 | self._telnet.write(data.encode('utf-8') + b'\n') 27 | return self._telnet.read_until(b"OK", timeout=5).decode('utf-8').strip() 28 | 29 | def is_live(self): 30 | result = self.send("ping") 31 | if "I am alive!" in result: 32 | return True 33 | else: 34 | return False 35 | 36 | def kill(self): 37 | self.send("kill") 38 | return True 39 | 40 | def wait_until_true(check_fn, timeout=10, description=None, interval=1): 41 | start_time = time.time() 42 | duration = 0 43 | while True: 44 | elapsed_seconds = time.time() - start_time 45 | if elapsed_seconds >= timeout: 46 | return False 47 | 48 | if check_fn(): 49 | return True 50 | else: 51 | if description: 52 | msg = description 53 | else: 54 | msg = "Wait until {} return True...".format(check_fn.__name__ + "()") 55 | 56 | elapsed_seconds = int(elapsed_seconds) 57 | if duration != elapsed_seconds: 58 | duration = elapsed_seconds 59 | logger.info("{}... {}s (timeout:{})".format(msg, elapsed_seconds, timeout)) 60 | 61 | time.sleep(interval) 62 | 63 | @pytest.fixture(scope="session") 64 | def serial(request): 65 | return device_serial 66 | 67 | @pytest.fixture(scope="session") 68 | def client(request): 69 | logger.info("Connecting to adb server {}:{}...".format(adb_host, adb_port)) 70 | client = AdbClient(host=adb_host, port=adb_port) 71 | 72 | def try_to_connect_to_adb_server(): 73 | try: 74 | client.version() 75 | return True 76 | except Exception: 77 | return False 78 | 79 | wait_until_true(try_to_connect_to_adb_server, 80 | timeout=60, 81 | description="Try to connect to adb server {}:{}".format(adb_host, adb_port)) 82 | 83 | logger.info("Adb server version: {}".format(client.version())) 84 | 85 | return client 86 | 87 | 88 | @pytest.fixture(scope="session") 89 | def device(request, client, serial): 90 | def emulator_console_is_connectable(): 91 | try: 92 | console = EmulatorConsole(host=adb_host, port=emulator_port) 93 | return console 94 | except Exception as e: 95 | return None 96 | 97 | def is_boot_completed(): 98 | try: 99 | adb_device = client.device(serial) 100 | result = adb_device.shell("getprop sys.boot_completed") 101 | if not result: 102 | return False 103 | 104 | result = int(result.strip()) 105 | 106 | if result == 1: 107 | return True 108 | else: 109 | return False 110 | except ValueError as e: 111 | logger.warning(e) 112 | return False 113 | except Exception as e: 114 | logger.error(e) 115 | return False 116 | 117 | result = wait_until_true(emulator_console_is_connectable, timeout=60) 118 | assert result, "Can't connect to the emulator console" 119 | 120 | result = wait_until_true(is_boot_completed, timeout=60) 121 | assert result, "The emulator doesn't boot" 122 | 123 | return AdbDevice(client, "emulator-5554") 124 | -------------------------------------------------------------------------------- /test/resources/apk/app-armeabi-v7a.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swind/pure-python-adb/b136202b04660db57b49418514dd9eddc2ecb365/test/resources/apk/app-armeabi-v7a.apk -------------------------------------------------------------------------------- /test/resources/apk/app-x86.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swind/pure-python-adb/b136202b04660db57b49418514dd9eddc2ecb365/test/resources/apk/app-x86.apk -------------------------------------------------------------------------------- /test/test_batterystats.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | @pytest.mark.skip 4 | def test_get_batterystats(device): 5 | assert device.get_batterystats() is not None 6 | 7 | def test_get_battery_level(device): 8 | result = device.get_battery_level() 9 | assert result == 100 10 | -------------------------------------------------------------------------------- /test/test_cpu_stat.py: -------------------------------------------------------------------------------- 1 | def test_get_cpu_times(device): 2 | result = device.cpu_times() 3 | assert result is not None 4 | 5 | def test_get_cpu_percent(device): 6 | percent = device.cpu_percent(interval=1) 7 | assert percent is not None 8 | assert percent != 0 9 | 10 | def test_get_cpu_count(device): 11 | assert device.cpu_count() == 2 12 | -------------------------------------------------------------------------------- /test/test_device.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | 4 | import pytest 5 | import socket 6 | 7 | from ppadb import ClearError, InstallError 8 | 9 | 10 | def test_install_uninstall_success(device): 11 | dir_path = os.path.dirname(os.path.realpath(__file__)) 12 | result = device.install(os.path.join(dir_path, "resources/apk/app-x86.apk"), 13 | reinstall=True, 14 | downgrade=True) 15 | assert result is True 16 | 17 | with pytest.raises(InstallError) as excinfo: 18 | device.install(os.path.join(dir_path, "resources/apk/app-x86.apk")) 19 | 20 | assert "INSTALL_FAILED_ALREADY_EXISTS" in str(excinfo.value) 21 | 22 | result = device.is_installed('com.cloudmosa.helloworldapk') 23 | assert result is True 24 | 25 | result = device.uninstall("com.cloudmosa.helloworldapk") 26 | assert result is True 27 | 28 | result = device.uninstall("com.cloudmosa.helloworldapk") 29 | assert result is False 30 | 31 | result = device.is_installed('com.cloudmosa.helloworldapk') 32 | assert result is False 33 | 34 | 35 | def test_uninstall_not_exist_package(device): 36 | result = device.uninstall("com.cloudmosa.not.exist") 37 | assert result is False 38 | 39 | 40 | def test_list_features(device): 41 | features = device.list_features() 42 | assert "reqGlEsVersion" in features 43 | assert "android.hardware.sensor.barometer" in features 44 | 45 | assert features["reqGlEsVersion"] == "0x20000" 46 | assert features["android.hardware.sensor.barometer"] is True 47 | 48 | 49 | def test_clear(device): 50 | result = device.clear("com.android.chrome") 51 | assert result is True 52 | 53 | with pytest.raises(ClearError) as excinfo: 54 | result = device.clear("com.android.not.exist.package") 55 | 56 | assert "Package com.android.not.exist.package could not be cleared - [Failed]" in str(excinfo.value) 57 | 58 | 59 | def test_list_packages(device): 60 | packages = device.list_packages() 61 | 62 | assert "com.android.chrome" in packages 63 | assert "com.android.shell" in packages 64 | 65 | 66 | def test_get_properties(device): 67 | properties = device.get_properties() 68 | 69 | assert "ro.product.device" in properties 70 | assert "ro.product.model" in properties 71 | 72 | 73 | def test_list_reverses(device): 74 | result = device.list_reverses() 75 | assert result is not None 76 | 77 | 78 | def test_reboot_than_wait_boot(device): 79 | result = device.reboot() 80 | assert device.wait_boot_complete() is True 81 | 82 | 83 | def test_get_version_name(device): 84 | result = device.get_package_version_name("com.android.chrome") 85 | assert result is not None 86 | 87 | 88 | def test_shell_echo_sleep_long_time(device): 89 | result = device.shell("sleep 30;echo passed") 90 | assert "passed" in result 91 | 92 | 93 | def test_shell_echo_timeout(device): 94 | with pytest.raises(socket.timeout) as excinfo: 95 | device.shell("sleep 60;echo passed", timeout=10) 96 | 97 | assert "timed out" in str(excinfo.value) 98 | 99 | 100 | def test_get_top_activity(device): 101 | activity = device.get_top_activity() 102 | assert activity is not None 103 | 104 | 105 | def test_get_top_activities(device): 106 | activities = device.get_top_activities() 107 | assert len(activities) != 0 108 | 109 | 110 | def test_pull(device): 111 | import hashlib 112 | 113 | device.shell("screencap -p /sdcard/screen.png") 114 | checksum = device.shell("md5sum -b /sdcard/screen.png").strip() 115 | 116 | device.pull("/sdcard/screen.png", "./screen.png") 117 | hash_md5 = hashlib.md5() 118 | with open("./screen.png", "rb") as fp: 119 | for chunk in iter(lambda: fp.read(4096), b""): 120 | hash_md5.update(chunk) 121 | 122 | pull_checksum = hash_md5.hexdigest() 123 | 124 | assert checksum == pull_checksum 125 | 126 | 127 | def test_push_stat(device): 128 | dir_path = os.path.dirname(os.path.realpath(__file__)) 129 | apk_path = os.path.join(dir_path, "resources/apk/app-x86.apk") 130 | device.push(apk_path, "/sdcard/test.apk") 131 | 132 | stat = os.stat(apk_path) 133 | result = device.shell("stat /sdcard/test.apk -c %X") 134 | if int(device.shell("getprop ro.build.version.sdk")) >= 27: 135 | assert int(result) == int(stat.st_mtime) 136 | else: 137 | timestamp = int(time.time()) 138 | assert timestamp - 10 <= int(result) <= timestamp + 10 139 | 140 | 141 | def test_push_dir(device): 142 | dir_path = os.path.dirname(os.path.realpath(__file__)) 143 | apk_path = os.path.join(dir_path, "resources/apk") 144 | device.push(apk_path, "/sdcard") 145 | 146 | result = device.shell("ls /sdcard/apk") 147 | assert "app-armeabi-v7a.apk" in result 148 | assert "app-x86.apk" in result 149 | 150 | 151 | def test_push_with_progress(device): 152 | dir_path = os.path.dirname(os.path.realpath(__file__)) 153 | apk_path = os.path.join(dir_path, "resources/apk/app-x86.apk") 154 | 155 | result = [] 156 | 157 | def progress(file_name, total_size, sent_size): 158 | result.append({ 159 | "file_name": file_name, 160 | "total_size": total_size, 161 | "sent_size": sent_size 162 | }) 163 | 164 | device.push(apk_path, "/sdcard/test.apk", progress=progress) 165 | 166 | assert result 167 | assert result[-1]["total_size"] == result[-1]["sent_size"] 168 | 169 | 170 | def test_forward(device): 171 | device.killforward_all() 172 | forward_map = device.list_forward() 173 | assert not forward_map 174 | 175 | device.forward("tcp:6000", "tcp:7000") 176 | device.forward("tcp:6001", "tcp:7001") 177 | device.forward("tcp:6002", "tcp:7002") 178 | 179 | forward_map = device.list_forward() 180 | assert forward_map['tcp:6000'] == "tcp:7000" 181 | assert forward_map['tcp:6001'] == "tcp:7001" 182 | assert forward_map['tcp:6002'] == "tcp:7002" 183 | 184 | device.killforward("tcp:6000") 185 | forward_map = device.list_forward() 186 | assert "tcp:6000" not in forward_map 187 | assert forward_map['tcp:6001'] == "tcp:7001" 188 | assert forward_map['tcp:6002'] == "tcp:7002" 189 | 190 | device.killforward_all() 191 | forward_map = device.list_forward() 192 | assert not forward_map 193 | 194 | 195 | @pytest.mark.skip 196 | def test_killforward_all(client, device): 197 | """ 198 | This testcase need two emulators for testing, 199 | But the android docker container can'd execute two emulators at the same time. 200 | If you want to execute this testcase, 201 | you need to start two emulators 'emulator-5554' and 'emualtor-5556' on your machine. 202 | """ 203 | device2 = client.device("emulator-5556") 204 | 205 | device.forward("tcp:6001", "tcp:6001") 206 | device2.forward("tcp:6002", "tcp:6002") 207 | 208 | forward_map = device.list_forward() 209 | assert forward_map['tcp:6001'] == "tcp:6001" 210 | assert "tcp:6002" not in forward_map 211 | 212 | device.killforward_all() 213 | forward_map = device.list_forward() 214 | assert "tcp:6001" not in forward_map 215 | 216 | forward_map = client.list_forward() 217 | assert "emulator-5556" in forward_map 218 | assert forward_map["emulator-5556"]["tcp:6002"] == "tcp:6002" 219 | 220 | client.killforward_all() 221 | forward_map = client.list_forward() 222 | assert not forward_map 223 | -------------------------------------------------------------------------------- /test/test_host.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | def test_list_devices(client, serial): 4 | devices = client.devices() 5 | assert len(devices) > 0 6 | assert any(map(lambda device: device.serial == serial, devices)) 7 | 8 | def test_list_devices_by_state(client): 9 | devices = client.devices(client.BOOTLOADER) 10 | assert len(devices) == 0 11 | 12 | devices = client.devices(client.OFFLINE) 13 | assert len(devices) == 0 14 | 15 | devices = client.devices(client.DEVICE) 16 | assert len(devices) == 1 17 | 18 | def test_version(client): 19 | version = client.version() 20 | 21 | assert type(version) == int 22 | assert version != 0 23 | 24 | def test_list_forward(client, device, serial): 25 | client.killforward_all() 26 | result = client.list_forward() 27 | assert not result 28 | 29 | device.forward("tcp:6000", "tcp:6000") 30 | result = client.list_forward() 31 | assert result[serial]["tcp:6000"] == "tcp:6000" 32 | 33 | client.killforward_all() 34 | result = client.list_forward() 35 | assert not result 36 | 37 | 38 | def test_features(client): 39 | assert client.features() 40 | 41 | 42 | def test_remote_connect_disconnect(client): 43 | host = client.host 44 | client.remote_connect(host, 5555) 45 | device = client.device("{}:5555".format(host)) 46 | assert device is not None 47 | 48 | # Disconnect by ip 49 | client.remote_disconnect(host) 50 | device = client.device("{}:5555".format(host)) 51 | assert device is None 52 | 53 | # Disconnect by ip and port 54 | client.remote_connect(host, 5555) 55 | device = client.device("{}:5555".format(host)) 56 | assert device is not None 57 | 58 | # Disconnect all 59 | client.remote_disconnect() 60 | device = client.device("{}:5555".format(host)) 61 | assert device is None 62 | 63 | for index in range(0, 10): 64 | device = client.device("emulator-5554") 65 | if device is not None: 66 | break 67 | else: 68 | time.sleep(1) 69 | 70 | assert device is not None 71 | -------------------------------------------------------------------------------- /test/test_host_serial.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import pytest 3 | 4 | 5 | def test_shell_sync(device): 6 | result = device.shell("dumpsys") 7 | assert "dumpsys window sessions" in result 8 | 9 | 10 | def test_shell_async(device): 11 | result = bytearray() 12 | 13 | def callback(conn): 14 | while True: 15 | data = conn.read(1024) 16 | if not data: 17 | break 18 | result.extend(bytearray(data)) 19 | 20 | thread = threading.Thread(target=device.shell, args=("dumpsys", callback)) 21 | thread.start() 22 | thread.join() 23 | 24 | assert "dumpsys window sessions" in result.decode('utf-8') 25 | 26 | 27 | def test_get_device_path(device): 28 | result = device.get_device_path() 29 | assert result == 'unknown' 30 | 31 | 32 | def test_get_serial_no(device, serial): 33 | result = device.get_serial_no() 34 | assert result == serial 35 | 36 | 37 | def test_get_state(device): 38 | result = device.get_state() 39 | assert result == 'device' 40 | 41 | 42 | def test_forward(device): 43 | device.forward("tcp:9999", "tcp:7777") 44 | forward_list = device.list_forward() 45 | assert "tcp:9999" in forward_list 46 | assert forward_list['tcp:9999'] == "tcp:7777" 47 | 48 | device.killforward("tcp:9999") 49 | forward_list = device.list_forward() 50 | assert len(forward_list) == 0 51 | 52 | 53 | def test_forward_killforward_all(device): 54 | device.forward("tcp:9999", "tcp:7777") 55 | forward_list = device.list_forward() 56 | assert "tcp:9999" in forward_list 57 | assert forward_list['tcp:9999'] == "tcp:7777" 58 | 59 | device.killforward_all() 60 | forward_list = device.list_forward() 61 | assert len(forward_list) == 0 62 | 63 | 64 | def test_forward_norebind_failed(device): 65 | try: 66 | device.forward("tcp:9999", "tcp:7777") 67 | forward_list = device.list_forward() 68 | assert "tcp:9999" in forward_list 69 | assert forward_list['tcp:9999'] == "tcp:7777" 70 | 71 | with pytest.raises(RuntimeError) as excinfo: 72 | device.forward("tcp:9999", "tcp:7777", norebind=True) 73 | 74 | assert "cannot rebind existing socket" in str(excinfo.value) 75 | finally: 76 | device.killforward_all() 77 | forward_list = device.list_forward() 78 | assert len(forward_list) == 0 79 | -------------------------------------------------------------------------------- /test/test_logging.py: -------------------------------------------------------------------------------- 1 | from ppadb.utils.logger import AdbLogging 2 | import logging 3 | 4 | def test_without_logging(capsys): 5 | logger = AdbLogging.get_logger("ppadb.test") 6 | logger.addHandler(logging.StreamHandler()) 7 | 8 | logger.info("INFO message") 9 | captured = capsys.readouterr() 10 | assert not captured.out 11 | assert not captured.err 12 | 13 | logger.warning("WARNING message") 14 | captured = capsys.readouterr() 15 | assert not captured.out 16 | assert not captured.err 17 | 18 | logger.debug("DEBUG message") 19 | assert not captured.out 20 | assert not captured.err 21 | 22 | def test_without_log_message_after_set_root_logger_level(capsys): 23 | logging.basicConfig() 24 | logger = AdbLogging.get_logger("ppadb.test") 25 | logger.addHandler(logging.StreamHandler()) 26 | 27 | logging.getLogger().setLevel(logging.DEBUG) 28 | 29 | logger.info("INFO message") 30 | captured = capsys.readouterr() 31 | assert not captured.out 32 | assert not captured.err 33 | 34 | logger.warning("WARNING message") 35 | captured = capsys.readouterr() 36 | assert not captured.out 37 | assert not captured.err 38 | 39 | logger.debug("DEBUG message") 40 | assert not captured.out 41 | assert not captured.err 42 | 43 | def test_enable_log_message(capsys): 44 | logging.basicConfig() 45 | logger = AdbLogging.get_logger("ppadb.test") 46 | logger.addHandler(logging.StreamHandler()) 47 | logging.getLogger("ppadb").setLevel(logging.DEBUG) 48 | 49 | logger.info("INFO message") 50 | captured = capsys.readouterr() 51 | assert not captured.out 52 | assert captured.err 53 | 54 | logger.warning("WARNING message") 55 | captured = capsys.readouterr() 56 | assert not captured.out 57 | assert captured.err 58 | 59 | logger.debug("DEBUG message") 60 | assert not captured.out 61 | assert captured.err 62 | 63 | -------------------------------------------------------------------------------- /test/test_plugins.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | 4 | def open_chrome(device): 5 | activity = "com.android.chrome/com.google.android.apps.chrome.Main" 6 | cmd = 'am start -a android.intent.action.VIEW -n {activity} -d {url}'.format( 7 | activity=activity, 8 | url="https://www.google.com" 9 | ) 10 | device.shell(cmd) 11 | 12 | 13 | def test_get_traffic(device): 14 | open_chrome(device) 15 | time.sleep(5) 16 | # Get chrome traffic state 17 | states = device.get_traffic("com.android.chrome") 18 | 19 | assert states is not None 20 | assert len(states) != 0 21 | 22 | 23 | def test_get_traffic_of_not_existing_package(device): 24 | states = device.get_traffic("com.not.existing.package") 25 | 26 | assert states is None 27 | 28 | 29 | def test_get_cpu_stat(device): 30 | open_chrome(device) 31 | 32 | pid = device.get_pid("com.android.chrome") 33 | assert pid is not None 34 | 35 | total_cpu_stat = device.get_total_cpu() 36 | process_cpu_stat = device.get_pid_cpu(pid) 37 | 38 | print("CPU Total: {}\n".format(total_cpu_stat)) 39 | print("CPU Process: {}\n".format(process_cpu_stat)) 40 | 41 | assert total_cpu_stat is not None 42 | assert process_cpu_stat is not None 43 | -------------------------------------------------------------------------------- /test/test_transport.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swind/pure-python-adb/b136202b04660db57b49418514dd9eddc2ecb365/test/test_transport.py -------------------------------------------------------------------------------- /test_async/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swind/pure-python-adb/b136202b04660db57b49418514dd9eddc2ecb365/test_async/__init__.py -------------------------------------------------------------------------------- /test_async/async_wrapper.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import warnings 3 | 4 | 5 | 6 | def _await(coro): 7 | with warnings.catch_warnings(record=True) as warns: 8 | ret = asyncio.get_event_loop().run_until_complete(coro) 9 | 10 | if warns: 11 | raise RuntimeError 12 | 13 | return ret 14 | 15 | 16 | def awaiter(func): 17 | def sync_func(*args, **kwargs): 18 | return _await(func(*args, **kwargs)) 19 | 20 | return sync_func 21 | -------------------------------------------------------------------------------- /test_async/patchers.py: -------------------------------------------------------------------------------- 1 | """Patches for async socket functionality.""" 2 | 3 | from contextlib import asynccontextmanager 4 | from unittest.mock import patch 5 | 6 | try: 7 | from unittest.mock import AsyncMock 8 | except ImportError: 9 | from unittest.mock import MagicMock 10 | 11 | class AsyncMock(MagicMock): 12 | async def __call__(self, *args, **kwargs): 13 | return super(AsyncMock, self).__call__(*args, **kwargs) 14 | 15 | 16 | def async_mock_open(read_data=""): 17 | class AsyncMockFile: 18 | def __init__(self, read_data): 19 | self.read_data = read_data 20 | _async_mock_open.written = read_data[:0] 21 | 22 | async def read(self, size=-1): 23 | if size == -1: 24 | ret = self.read_data 25 | self.read_data = self.read_data[:0] 26 | return ret 27 | 28 | n = min(size, len(self.read_data)) 29 | ret = self.read_data[:n] 30 | self.read_data = self.read_data[n:] 31 | return ret 32 | 33 | async def write(self, b): 34 | if _async_mock_open.written: 35 | _async_mock_open.written += b 36 | else: 37 | _async_mock_open.written = b 38 | 39 | @asynccontextmanager 40 | async def _async_mock_open(*args, **kwargs): 41 | try: 42 | yield AsyncMockFile(read_data) 43 | finally: 44 | pass 45 | 46 | return _async_mock_open 47 | 48 | 49 | class FakeStreamWriter: 50 | def close(self): 51 | pass 52 | 53 | async def wait_closed(self): 54 | pass 55 | 56 | def write(self, data): 57 | pass 58 | 59 | async def drain(self): 60 | pass 61 | 62 | 63 | class FakeStreamReader: 64 | async def read(self, numbytes): 65 | return b'TEST' 66 | 67 | 68 | def async_patch(*args, **kwargs): 69 | return patch(*args, new_callable=AsyncMock, **kwargs) 70 | -------------------------------------------------------------------------------- /test_async/test_client_async.py: -------------------------------------------------------------------------------- 1 | """Unit tests for the `ClientAsync` class. 2 | 3 | """ 4 | 5 | 6 | import asyncio 7 | import sys 8 | import unittest 9 | 10 | sys.path.insert(0, '..') 11 | 12 | from ppadb.client_async import ClientAsync 13 | 14 | from .async_wrapper import awaiter 15 | from .patchers import FakeStreamReader, FakeStreamWriter, async_patch 16 | 17 | 18 | class TestClientAsync(unittest.TestCase): 19 | def setUp(self): 20 | self.client = ClientAsync() 21 | 22 | @awaiter 23 | async def test_create_connection_fail(self): 24 | with self.assertRaises(RuntimeError): 25 | await self.client.create_connection() 26 | 27 | @awaiter 28 | async def test_device_returns_none(self): 29 | with async_patch('asyncio.open_connection', return_value=(FakeStreamReader(), FakeStreamWriter())): 30 | with async_patch('{}.FakeStreamReader.read'.format(__name__), side_effect=[b'OKAY', b'0000', b'']): 31 | self.assertIsNone(await self.client.device('serial')) 32 | 33 | @awaiter 34 | async def test_device(self): 35 | with async_patch('asyncio.open_connection', return_value=(FakeStreamReader(), FakeStreamWriter())): 36 | with async_patch('{}.FakeStreamReader.read'.format(__name__), side_effect=[b'OKAY', b'000b', b'serial test']): 37 | self.assertIsNotNone(await self.client.device('serial')) 38 | 39 | 40 | if __name__ == '__main__': 41 | unittest.main() 42 | -------------------------------------------------------------------------------- /test_async/test_connection_async.py: -------------------------------------------------------------------------------- 1 | """Unit tests for `ConnectionAsync` class. 2 | 3 | """ 4 | 5 | 6 | import asyncio 7 | import sys 8 | import unittest 9 | from unittest.mock import patch 10 | 11 | sys.path.insert(0, '..') 12 | 13 | from ppadb.connection_async import ConnectionAsync 14 | 15 | from .async_wrapper import awaiter 16 | from .patchers import FakeStreamReader, FakeStreamWriter, async_patch 17 | 18 | 19 | class TestConnectionAsync(unittest.TestCase): 20 | @awaiter 21 | async def test_connect_close(self): 22 | with async_patch('asyncio.open_connection', return_value=(FakeStreamReader(), FakeStreamWriter())): 23 | conn = ConnectionAsync() 24 | await conn.connect() 25 | self.assertIsNotNone(conn.reader) 26 | self.assertIsNotNone(conn.writer) 27 | 28 | await conn.close() 29 | self.assertIsNone(conn.reader) 30 | self.assertIsNone(conn.writer) 31 | 32 | @awaiter 33 | async def test_connect_close_catch_oserror(self): 34 | with async_patch('asyncio.open_connection', return_value=(FakeStreamReader(), FakeStreamWriter())): 35 | conn = ConnectionAsync() 36 | await conn.connect() 37 | self.assertIsNotNone(conn.reader) 38 | self.assertIsNotNone(conn.writer) 39 | 40 | with patch('{}.FakeStreamWriter.close'.format(__name__), side_effect=OSError): 41 | await conn.close() 42 | self.assertIsNone(conn.reader) 43 | self.assertIsNone(conn.writer) 44 | 45 | @awaiter 46 | async def test_connect_with_timeout(self): 47 | with self.assertRaises(RuntimeError): 48 | with async_patch('asyncio.open_connection', side_effect=asyncio.TimeoutError): 49 | conn = ConnectionAsync(timeout=1) 50 | await conn.connect() 51 | 52 | 53 | if __name__ == '__main__': 54 | unittest.main() 55 | -------------------------------------------------------------------------------- /test_async/test_device_async.py: -------------------------------------------------------------------------------- 1 | """Unit tests for the `DeviceAsync` class. 2 | 3 | """ 4 | 5 | 6 | import asyncio 7 | from contextlib import asynccontextmanager 8 | import os 9 | import sys 10 | import unittest 11 | from unittest.mock import mock_open, patch 12 | 13 | sys.path.insert(0, '..') 14 | 15 | from ppadb.client_async import ClientAsync 16 | from ppadb.protocol import Protocol 17 | from ppadb.sync_async import SyncAsync 18 | 19 | from .async_wrapper import awaiter 20 | from .patchers import FakeStreamReader, FakeStreamWriter, async_mock_open, async_patch 21 | 22 | 23 | PNG_IMAGE = b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\n\x00\x00\x00\n\x08\x06\x00\x00\x00\x8d2\xcf\xbd\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\x00\x00\x00\tpHYs\x00\x00\x0fa\x00\x00\x0fa\x01\xa8?\xa7i\x00\x00\x00\x0eIDAT\x18\x95c`\x18\x05\x83\x13\x00\x00\x01\x9a\x00\x01\x16\xca\xd3i\x00\x00\x00\x00IEND\xaeB`\x82' 24 | 25 | PNG_IMAGE_NEEDS_REPLACING = PNG_IMAGE[:5] + b'\r' + PNG_IMAGE[5:] 26 | 27 | FILEDATA = b'Ohayou sekai.\nGood morning world!' 28 | 29 | 30 | class TestDevice(unittest.TestCase): 31 | @awaiter 32 | async def setUp(self): 33 | self.client = ClientAsync() 34 | 35 | with async_patch('asyncio.open_connection', return_value=(FakeStreamReader(), FakeStreamWriter())): 36 | with async_patch('{}.FakeStreamReader.read'.format(__name__), side_effect=[b'OKAY', b'000b', b'serial test']): 37 | self.device = await self.client.device('serial') 38 | 39 | @awaiter 40 | async def test_shell(self): 41 | with async_patch('asyncio.open_connection', return_value=(FakeStreamReader(), FakeStreamWriter())): 42 | with async_patch('{}.FakeStreamReader.read'.format(__name__), side_effect=[b'OKAY', b'OKAY', b'test', b'', b'OKAY']): 43 | self.assertEqual(await self.device.shell('TEST'), 'test') 44 | 45 | @awaiter 46 | async def test_shell_error(self): 47 | with async_patch('asyncio.open_connection', return_value=(FakeStreamReader(), FakeStreamWriter())): 48 | with async_patch('{}.FakeStreamReader.read'.format(__name__), return_value=b'FAIL'): 49 | with self.assertRaises(RuntimeError): 50 | self.assertEqual(await self.device.shell('TEST'), 'test') 51 | 52 | @awaiter 53 | async def test_screencap(self): 54 | with async_patch('asyncio.open_connection', return_value=(FakeStreamReader(), FakeStreamWriter())): 55 | with async_patch('{}.FakeStreamReader.read'.format(__name__), side_effect=[b'OKAY', b'OKAY', PNG_IMAGE, b'', b'OKAY']): 56 | self.assertEqual(await self.device.screencap(), PNG_IMAGE) 57 | 58 | with async_patch('asyncio.open_connection', return_value=(FakeStreamReader(), FakeStreamWriter())): 59 | with async_patch('{}.FakeStreamReader.read'.format(__name__), side_effect=[b'OKAY', b'OKAY', PNG_IMAGE_NEEDS_REPLACING, b'', b'OKAY']): 60 | self.assertEqual(await self.device.screencap(), PNG_IMAGE) 61 | 62 | @awaiter 63 | async def test_push_file_not_found(self): 64 | with patch('os.path.exists', return_value=False): 65 | with self.assertRaises(FileNotFoundError): 66 | await self.device.push('src', 'dest') 67 | 68 | with self.assertRaises(FileNotFoundError): 69 | sync = SyncAsync('Unused') 70 | await sync.push('src', 'dest', 'mode') 71 | 72 | @awaiter 73 | async def test_push(self): 74 | def progress(*args, **kwargs): 75 | pass 76 | 77 | filedata = b'Ohayou sekai.\nGood morning world!' 78 | with patch('os.path.exists', return_value=True), patch('os.path.isfile', return_value=True), patch('os.stat', return_value=os.stat_result((123,) * 10)), patch('ppadb.sync_async.aiofiles.open', async_mock_open(FILEDATA)): 79 | with async_patch('asyncio.open_connection', return_value=(FakeStreamReader(), FakeStreamWriter())): 80 | with async_patch('{}.FakeStreamReader.read'.format(__name__), side_effect=[b'OKAY', b'OKAY', b'OKAY', PNG_IMAGE_NEEDS_REPLACING, b'', b'OKAY']): 81 | await self.device.push('src', 'dest', progress=progress) 82 | 83 | @awaiter 84 | async def test_push_dir(self): 85 | with patch('os.path.exists', return_value=True), patch('os.path.isfile', return_value=False), patch('os.path.isdir', return_value=True), patch('os.walk', return_value=[('root1', 'dirs1', 'files1'), ('root2', 'dirs2', 'files2')]): 86 | with async_patch('ppadb.device_async.DeviceAsync.shell'), async_patch('ppadb.device_async.DeviceAsync._push'): 87 | await self.device.push('src', 'dest') 88 | 89 | @awaiter 90 | async def test_pull(self): 91 | with async_patch('asyncio.open_connection', return_value=(FakeStreamReader(), FakeStreamWriter())): 92 | with async_patch('{}.FakeStreamReader.read'.format(__name__), side_effect=[b'OKAY', b'OKAY', b'DATA', SyncAsync._little_endian(4), b'TEST', b'DONE', b'OKAY']): 93 | with patch('ppadb.sync_async.aiofiles.open', async_mock_open(FILEDATA)): 94 | await self.device.pull('src', 'dest') 95 | 96 | @awaiter 97 | async def test_pull_fail(self): 98 | with async_patch('asyncio.open_connection', return_value=(FakeStreamReader(), FakeStreamWriter())): 99 | with async_patch('{}.FakeStreamReader.read'.format(__name__), side_effect=[b'OKAY', b'OKAY', b'FAIL', SyncAsync._little_endian(4), b'TEST', b'DONE', b'OKAY']): 100 | with patch('ppadb.sync_async.aiofiles.open', async_mock_open(FILEDATA)): 101 | await self.device.pull('src', 'dest') 102 | 103 | 104 | class TestProtocol(unittest.TestCase): 105 | def test_encode_decode_length(self): 106 | for i in range(16 ** 2): 107 | self.assertEqual(i, Protocol.decode_length(Protocol.encode_length(i))) 108 | 109 | 110 | if __name__ == '__main__': 111 | unittest.main() 112 | --------------------------------------------------------------------------------