def scan_phone_tcp(to_search_path, remote_ip, adb_key_file, max_files=None): """ Search in the given directory for .jpg files and copy them to a temporarily folder. By default the 5555 port is used. After comparison, they're deleted from the original path. :param to_search_path: The path where the files are located. :param remote_ip: The IP of the device where the files are located. :param adb_key_file: The ADB key of the device to be connected. :param max_files: The number of files to be moved. :return: True """ android_images = [] with open(adb_key_file) as f: priv = f.read() signer = PythonRSASigner('', priv) device = AdbDeviceTcp(remote_ip, 5555, default_transport_timeout_s=100.) if device.connect(rsa_keys=[signer], auth_timeout_s=100.): if device.available: print("Connected to selected device.\n---") directory_scan = device.list(to_search_path, None, 9000) if max_files is None: max_files = len(directory_scan) for file in directory_scan: if os.path.splitext(file.filename.decode('utf-8'))[1] == ".jpg": save = AndroidPhoto() save.name = file.filename.decode("utf-8") save.size = file.size save.path = to_search_path android_images.append(save) if len(android_images) >= max_files: break print(f"There're listed {len(android_images)} files.\n---") for image in android_images: device.pull(image.path + image.name, os.path.join(temp_directory, image.name), progress_callback=log_pull_status, transport_timeout_s=100, read_timeout_s=100) if image.size == os.path.getsize(os.path.join(temp_directory, image.name)): device.shell('rm -f ' + to_search_path + image.name) print("\r\r" + image.name + " is now in the temp folder.") print("---\nAll files are now in the temp folder.\n---") return True
def get_apk_file(adbkey_path, device_ip, device_port, package_name): with open(adbkey_path) as f: priv = f.read() signer = PythonRSASigner('', priv) device = AdbDeviceTcp(device_ip, device_port, default_timeout_s=9.) device.connect(rsa_keys=[signer], auth_timeout_s=0.1) # Send a shell command # apk file (base.apk) print("<*> Copying APK file to /data/local/tmp/apkSneeze/base.apk (mobile device)...") shell_cmd = "su - root -c '\ mkdir -p /data/local/tmp/apkSneeze && \ cp /data/app/{}*/base.apk /data/local/tmp/apkSneeze && \ chmod 777 /data/local/tmp/apkSneeze/base.apk'".format(package_name) device.shell(shell_cmd) print("<*> Downloading APK file to {}...".format(DEFAULT_APP_NAME)) device.pull("/data/local/tmp/apkSneeze/base.apk", DEFAULT_APP_NAME) print("<*> Download done, check {}".format(DEFAULT_APP_NAME)) print("<*> Deleting APK file from /data/local/tmp/apkSneeze/base.apk (mobile device)...") device.shell("su - root -c 'rm /data/local/tmp/apkSneeze/base.apk'")
class ADBPython(object): """A manager for ADB connections that uses a Python implementation of the ADB protocol. Parameters ---------- host : str The address of the device; may be an IP address or a host name port : int The device port to which we are connecting (default is 5555) adbkey : str The path to the ``adbkey`` file for ADB authentication """ def __init__(self, host, port, adbkey=''): self.host = host self.port = int(port) self.adbkey = adbkey self._adb = AdbDeviceTcp(host=self.host, port=self.port, default_timeout_s=9.) # keep track of whether the ADB connection is intact self._available = False # use a lock to make sure that ADB commands don't overlap self._adb_lock = threading.Lock() @property def available(self): """Check whether the ADB connection is intact. Returns ------- bool Whether or not the ADB connection is intact """ return self._adb.available def close(self): """Close the ADB socket connection. """ self._adb.close() def connect(self, always_log_errors=True, auth_timeout_s=DEFAULT_AUTH_TIMEOUT_S): """Connect to an Android TV / Fire TV device. Parameters ---------- always_log_errors : bool If True, errors will always be logged; otherwise, errors will only be logged on the first failed reconnect attempt auth_timeout_s : float Authentication timeout (in seconds) Returns ------- bool Whether or not the connection was successfully established and the device is available """ try: with _acquire(self._adb_lock): # Catch exceptions try: # Connect with authentication if self.adbkey: # private key with open(self.adbkey) as f: priv = f.read() # public key try: with open(self.adbkey + '.pub') as f: pub = f.read() except FileNotFoundError: pub = '' signer = PythonRSASigner(pub, priv) self._adb.connect(rsa_keys=[signer], auth_timeout_s=auth_timeout_s) # Connect without authentication else: self._adb.connect(auth_timeout_s=auth_timeout_s) # ADB connection successfully established _LOGGER.debug( "ADB connection to %s:%d successfully established", self.host, self.port) self._available = True return True except OSError as exc: if self._available or always_log_errors: if exc.strerror is None: exc.strerror = "Timed out trying to connect to ADB device." _LOGGER.warning("Couldn't connect to %s:%d. %s: %s", self.host, self.port, exc.__class__.__name__, exc.strerror) # ADB connection attempt failed self.close() self._available = False return False except Exception as exc: # pylint: disable=broad-except if self._available or always_log_errors: _LOGGER.warning("Couldn't connect to %s:%d. %s: %s", self.host, self.port, exc.__class__.__name__, exc) # ADB connection attempt failed self.close() self._available = False return False except LockNotAcquiredException: _LOGGER.warning( "Couldn't connect to %s:%d because adb-shell lock not acquired.", self.host, self.port) self.close() self._available = False return False def pull(self, local_path, device_path): """Pull a file from the device using the Python ADB implementation. Parameters ---------- local_path : str The path where the file will be saved device_path : str The file on the device that will be pulled """ if not self.available: _LOGGER.debug( "ADB command not sent to %s:%d because adb-shell connection is not established: pull(%s, %s)", self.host, self.port, local_path, device_path) return with _acquire(self._adb_lock): _LOGGER.debug( "Sending command to %s:%d via adb-shell: pull(%s, %s)", self.host, self.port, local_path, device_path) self._adb.pull(device_path, local_path) return def push(self, local_path, device_path): """Push a file to the device using the Python ADB implementation. Parameters ---------- local_path : str The file that will be pushed to the device device_path : str The path where the file will be saved on the device """ if not self.available: _LOGGER.debug( "ADB command not sent to %s:%d because adb-shell connection is not established: push(%s, %s)", self.host, self.port, local_path, device_path) return with _acquire(self._adb_lock): _LOGGER.debug( "Sending command to %s:%d via adb-shell: push(%s, %s)", self.host, self.port, local_path, device_path) self._adb.push(local_path, device_path) return def shell(self, cmd): """Send an ADB command using the Python ADB implementation. Parameters ---------- cmd : str The ADB command to be sent Returns ------- str, None The response from the device, if there is a response """ if not self.available: _LOGGER.debug( "ADB command not sent to %s:%d because adb-shell connection is not established: %s", self.host, self.port, cmd) return None with _acquire(self._adb_lock): _LOGGER.debug("Sending command to %s:%d via adb-shell: %s", self.host, self.port, cmd) return self._adb.shell(cmd)
class AdbConnector: """ wraps adb_shell functions for simpler automation """ def __init__(self, ip=None, need_auth=True, device_name="", auto_reconnect_seconds=60, verbose=False): self.ip = ip self.device_name = device_name self.need_auth = need_auth self.auto_reconnect_seconds = auto_reconnect_seconds self.verbose=verbose self._event_handler = AdbConnectorEventManager() self._last_connected_time = time.time() self._device_resolution = None self._device = None self.width = self.screen_width() self.height = self.screen_height() print(f">>> Target phone sreen size is [{self.width} x {self.height}]") def _connect(self): """ in my experience, it was better to connect, send commands, disconnect right away rather than connect once and send many commands for hours => getting very slow at some point Now added auto-reconnect feature with argument auto_reconnect_seconds defaulting to 1 min """ now = time.time() if self._device and self._last_connected_time + self.auto_reconnect_seconds < now: self._disconnect() if self._device: return if self.verbose: print(f">>> connecting to {self.ip}") self._last_connected_time = now self._device = AdbDeviceTcp(self.ip, default_timeout_s=self.auto_reconnect_seconds) if not self.need_auth: self._device.connect(auth_timeout_s=0.1) else: with open(os.path.expanduser('~/.android/adbkey')) as f: private_key = f.read() signer = PythonRSASigner('', private_key) self._device.connect(rsa_keys=[signer], auth_timeout_s=0.1) if self.verbose: print(f">>> connected") def _disconnect(self): self._device.close() self._device = None if self.verbose: print(">>> disconnected") def _shell(self, command, **kwargs): if self.verbose: print(f">>> shell {command}") return self._device.shell(command, **kwargs) def _pull(self, from_file, to_file): if self.verbose: print(f">>> pull {from_file} to {to_file}") return self._device.pull(from_file, to_file) def tap(self, x, y, wait_ms = 100): """ tap the screen and force wait 100ms by default to simulate real taps if several in a row :param x: :param y: :param wait_ms: :return: """ assert 0 <= x < self.width, "out of bound" assert 0 <= y < self.height, "out of bound" self._connect() self._shell(f'{CMD_SHELL_TAP} {x:.0f} {y:.0f}') self.wait(wait_ms) def press(self, x, y, time_ms = 0, wait_ms = 0): """ long tap, implemented with a static swipe :param x: :param y: :param time_ms: :param wait_ms: :return: """ self._connect() self._shell(f'{CMD_SHELL_SWIPE} {x:.0f} {y:.0f} {x:.0f} {y:.0f} {time_ms}') self.wait(wait_ms) def swipe(self, x1, y1, x2, y2, time_ms = 0, wait_ms = 0): """ swipe from point (x1, y1) to point (x2, y2) in the span of time_ms careful, swipe has inertia, so if it is used to scroll a screen for example, screen will most likely keep scrolling at the end of the swipe To avoid that effect, use the event_move() instead :param x1: :param y1: :param x2: :param y2: :param time_ms: :param wait_ms: :return: """ self._connect() print(x1, y1, x2, y2) self._shell(f'{CMD_SHELL_SWIPE} {x1:.0f} {y1:.0f} {x2:.0f} {y2:.0f} {time_ms}') self.wait(wait_ms) @staticmethod def wait(time_ms): """ to wait some time. Not relying on adb, just convenient :param time_ms: :return: """ if time_ms > 0: time.sleep(time_ms / 1000.0) @DeprecationWarning def event_press(self, slot, x, y): self._event_handler.event_press(slot, x, y) @DeprecationWarning def event_move(self, slot, x, y): self._event_handler.event_move(slot, x, y) @DeprecationWarning def event_release(self, slot): self._event_handler.event_release(slot) @DeprecationWarning def event_flush(self): self._connect() self._device.shell(self._event_handler.event_flush(self.device_name)) # too much info: debug only def print_all_process_info(self): self._connect() processes_with_focus = self._shell(CMD_WINDOWS_DUMP) print(processes_with_focus) # todo: does not work anymore with latest android versions? @DeprecationWarning def process_has_focus(self, process_name): self._connect() all_processes = self._shell(f"{CMD_WINDOWS_DUMP} | grep -i {process_name} | grep -i mcurrentfocus") return len(all_processes) > 0 # todo: needs PR merged in adb_shell module def listen(self): self._connect() for line in self._device.streaming_shell(CMD_GET_EVENT): print(line) def _get_screen_resolution(self): if not self._device_resolution: self._connect() header_width_x_height = self._shell(CMD_WM_SIZE) self._device_resolution = tuple(map(int, re.findall("\d+", header_width_x_height))) return self._device_resolution def screen_width(self): return self._get_screen_resolution()[0] def screen_height(self): return self._get_screen_resolution()[1] def get_screenshot(self, raw=False, pull=True) -> Image: if pull: if raw: return self._get_screenshot_raw_pull_file() return self._get_screenshot_png_pull_file() if raw: raise Exception("Not implemented") return self._get_screenshot_png_stream() @staticmethod def get_temp_remote_filepath(extension): random_part = next(tempfile._get_candidate_names()) return os.path.join(REMOTE_SCREENSHOT_DIRECTORY, f"screenshot_adb_{random_part}.{extension}") @staticmethod def get_temp_local_filepath(extension): if not os.path.exists(OUTPUT_DIR): os.mkdir(OUTPUT_DIR) return os.path.realpath(f"{OUTPUT_DIR}/screenshot.{extension}") def _get_screenshot_png_pull_file(self) -> Image: self._connect() png_remote_filepath = self.get_temp_remote_filepath("png") png_local_filepath = self.get_temp_local_filepath("png") self._shell(f"{CMD_SCREENSHOT_PNG} {png_remote_filepath}") self._pull(png_remote_filepath, png_local_filepath) self._shell(f"rm {png_remote_filepath}") return Image.open(png_local_filepath) def _get_screenshot_raw_pull_file(self) -> Image: self._connect() raw_remote_filepath = self.get_temp_remote_filepath("raw") raw_local_filepath = self.get_temp_local_filepath("raw") self._shell(f"{CMD_SCREENSHOT_RAW} {raw_remote_filepath}") self._pull(raw_remote_filepath, raw_local_filepath) self._shell(f"rm {raw_remote_filepath}") with open(raw_local_filepath, 'rb') as f: raw = f.read() return Image.frombuffer('RGBA', (self.screen_height(), self.screen_width()), raw[12:], 'raw', 'RGBX', 0, 1) # todo: use exec-out instead of shell def _get_screenshot_png_stream(self) -> Image: self._connect() raw = self._shell(CMD_SCREENSHOT_PNG, decode=False) image = Image.open(io.BytesIO(raw)) return image