def screencap_h264(device: AdbDevice, executor: Executor): start = time() device.push("../scrcpy-win64/scrcpy-server.jar", "/data/local/tmp/scrcpy-server.jar") print("Pushed server component") sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) port = device.adb_output("reverse", "tcp:5556", "localabstract:scrcpy") print("[ADB]", port) # sock.bind(("127.0.0.1", 5556)) # sock.listen() executor.submit(lambda: print( "[SCRCPY]", device.shell("CLASSPATH=/data/local/tmp/scrcpy-server.jar " "app_process / com.genymobile.scrcpy.Server 0 8000000 " "false - false true"))) print("Executed scrcpy") print("Waiting for connection") # conn, info = sock.accept() # print(info) # print(conn.recv(1)) # print(conn.recv(64)) # print(conn.recv(2)) # print(conn.recv(2)) # print("Closing transmission") # device.shell("\x03") # conn.close() # sock.close() print("Recv h264 took %f seconds" % (time() - start))
def _setup_minicap(d: adbutils.AdbDevice): def cache_download(url, dst): if os.path.exists(dst): print("Use cached", dst) return print("Download {} from {}".format(dst, url)) resp = requests.get(url, stream=True) resp.raise_for_status() length = int(resp.headers.get("Content-Length", 0)) r = ReadProgress(resp.raw, length) with open(dst + ".cached", "wb") as f: shutil.copyfileobj(r, f) shutil.move(dst + ".cached", dst) def push_zipfile(path: str, dest: str, mode=0o755, zipfile_path: str = "vendor/stf-binaries-master.zip"): """ push minicap and minitouch from zip """ with zipfile.ZipFile(zipfile_path) as z: if path not in z.namelist(): print("WARNING: stf stuff %s not found", path) return with z.open(path) as f: d.sync.push(f, dest, mode) zipfile_path = "stf-binaries.zip" cache_download("https://github.com/openatx/stf-binaries/archive/0.2.zip", zipfile_path) zip_folder = "stf-binaries-0.2" sdk = d.getprop("ro.build.version.sdk") # eg 26 abi = d.getprop('ro.product.cpu.abi') # eg arm64-v8a abis = (d.getprop('ro.product.cpu.abilist').strip() or abi).split(",") # return print("sdk: %s, abi: %s, support-abis: %s" % (sdk, abi, ','.join(abis))) print("Push minicap+minicap.so to device") prefix = zip_folder + "/node_modules/minicap-prebuilt/prebuilt/" push_zipfile(prefix + abi + "/lib/android-" + sdk + "/minicap.so", "/data/local/tmp/minicap.so", 0o644, zipfile_path) push_zipfile(prefix + abi + "/bin/minicap", "/data/local/tmp/minicap", 0o0755, zipfile_path) print("Push minitouch to device") prefix = zip_folder + "/node_modules/minitouch-prebuilt/prebuilt/" push_zipfile(prefix + abi + "/bin/minitouch", "/data/local/tmp/minitouch", 0o0755, zipfile_path) # check if minicap installed output = d.shell( ["LD_LIBRARY_PATH=/data/local/tmp", "/data/local/tmp/minicap", "-i"]) print(output) print("If you see JSON output, it means minicap installed successfully")
def handle_new_ad(ad: Tuple[Tuple[int, int], np.ndarray], device: AdbDevice, state: dict): """ Handles a newly identified ad, whether or not there actually is one. This method also makes sure, that ads are closed again. Some ads open the Play Store, so this method also moves back to Nimses. :param ad: a tuple with the ad location (optional) and the screen shot. :param device: the Android device that should be controlled :param state: the global state of this application """ def close_ad(): """ Closes the currently open ad (really just switches to the main activity) """ if not state["ad_closed"] and state["ad_time"] < time(): device.app_start(NIMSES_PACKAGE, NIMSES_MAIN_ACTIVITY) state["ad_closed"] = True logger.info("Closed ad") pt, image = ad app = device.current_app() logger.info("Current app %s", app) logger.info("Ad open? %s", not state["ad_closed"]) if np.array_equal(image, state["last_image"]) \ or state["last_ad"] > 10: output = device.shell(["am", "force-stop", NIMSES_PACKAGE]) logger.info(f"[ADB] {output}") device.app_start(NIMSES_PACKAGE, NIMSES_MAIN_ACTIVITY) state["last_ad"] = 0 state["ad_closed"] = True logger.info("Restarted Nimses") elif app["package"] == NIMSES_PACKAGE: if app["activity"] == NIMSES_MAIN_ACTIVITY: # Ad found, click it if pt: device.click(pt[0], pt[1]) state["last_ad"] = 0 state["ad_closed"] = True logger.info("Clicked %d, %d", pt[0], pt[1]) # No ad, scroll along else: state["last_ad"] += 1 w, h = device.window_size() sx = w // 2 sy = h // 4 * 3 dx = w // 2 dy = h // 4 * 1 device.swipe(sx, sy, dx, dy, 1.0) logger.info("Scrolled from %d %d to %d %d", sx, sy, dx, dy) elif app["activity"] in NIMSES_AD_ACTIVITIES: if state["ad_closed"]: state["ad_time"] = time() + 35 state["ad_closed"] = False logger.info("Close ad at: %s", strftime("%H:%M:%S", localtime(state["ad_time"]))) else: close_ad() elif app["package"] == PLAYSTORE_PACKAGE: device.app_start(NIMSES_PACKAGE, NIMSES_MAIN_ACTIVITY) logger.info("Closed Play Store") else: device.app_start(NIMSES_PACKAGE, NIMSES_MAIN_ACTIVITY)
def test_adb_connect(device: adbutils.AdbDevice): with pytest.raises(adbutils.AdbTimeout): device.shell("sleep 10", timeout=1.0)
class Connection: config: AzurLaneConfig serial: str adb_binary_list = [ './bin/adb/adb.exe', './toolkit/Lib/site-packages/adbutils/binaries/adb.exe', '/usr/bin/adb' ] def __init__(self, config): """ Args: config (AzurLaneConfig, str): Name of the user config under ./config """ logger.hr('Device') if isinstance(config, str): self.config = AzurLaneConfig(config, task=None) else: self.config = config self.serial = str(self.config.Emulator_Serial) if "bluestacks4-hyperv" in self.serial: self.serial = self.find_bluestacks4_hyperv(self.serial) if "bluestacks5-hyperv" in self.serial: self.serial = self.find_bluestacks5_hyperv(self.serial) if "127.0.0.1:58526" in self.serial: raise RequestHumanTakeover('Serial 127.0.0.1:58526 seems to be WSA, please use "wsa-0" or others to instead') if "wsa" in self.serial: self.serial = '127.0.0.1:58526' logger.attr('Adb_binary', self.adb_binary) # Monkey patch to custom adb adbutils.adb_path = lambda: self.adb_binary # Remove global proxies, or uiautomator2 will go through it for k in list(os.environ.keys()): if k.lower().endswith('_proxy'): del os.environ[k] self.adb_client = AdbClient('127.0.0.1', 5037) self.adb_connect(self.serial) self.adb = AdbDevice(self.adb_client, self.serial) logger.attr('Adb_device', self.adb) @staticmethod def find_bluestacks4_hyperv(serial): """ Find dynamic serial of Bluestacks4 Hyper-v Beta. Args: serial (str): 'bluestacks4-hyperv', 'bluestacks4-hyperv-2' for multi instance, and so on. Returns: str: 127.0.0.1:{port} """ from winreg import ConnectRegistry, OpenKey, QueryInfoKey, EnumValue, CloseKey, HKEY_LOCAL_MACHINE logger.info("Use Bluestacks4 Hyper-v Beta") if serial == "bluestacks4-hyperv": folder_name = "Android" else: folder_name = f"Android_{serial[19:]}" logger.info("Reading Realtime adb port") reg_root = ConnectRegistry(None, HKEY_LOCAL_MACHINE) sub_dir = f"SOFTWARE\\BlueStacks_bgp64_hyperv\\Guests\\{folder_name}\\Config" bs_keys = OpenKey(reg_root, sub_dir) bs_keys_count = QueryInfoKey(bs_keys)[1] for i in range(bs_keys_count): key_name, key_value, key_type = EnumValue(bs_keys, i) if key_name == "BstAdbPort": logger.info(f"New adb port: {key_value}") serial = f"127.0.0.1:{key_value}" break CloseKey(bs_keys) CloseKey(reg_root) return serial @staticmethod def find_bluestacks5_hyperv(serial): """ Find dynamic serial of Bluestacks5 Hyper-v. Args: serial (str): 'bluestacks5-hyperv', 'bluestacks5-hyperv-1' for multi instance, and so on. Returns: str: 127.0.0.1:{port} """ from winreg import ConnectRegistry, OpenKey, QueryInfoKey, EnumValue, CloseKey, HKEY_LOCAL_MACHINE logger.info("Use Bluestacks5 Hyper-v") logger.info("Reading Realtime adb port") if serial == "bluestacks5-hyperv": parameter_name = "bst.instance.Nougat64.status.adb_port" else: parameter_name = f"bst.instance.Nougat64_{serial[19:]}.status.adb_port" reg_root = ConnectRegistry(None, HKEY_LOCAL_MACHINE) sub_dir = f"SOFTWARE\\BlueStacks_nxt" bs_keys = OpenKey(reg_root, sub_dir) bs_keys_count = QueryInfoKey(bs_keys)[1] for i in range(bs_keys_count): key_name, key_value, key_type = EnumValue(bs_keys, i) if key_name == "UserDefinedDir": logger.info(f"Configuration file directory: {key_value}") with open(f"{key_value}\\bluestacks.conf", 'r', encoding='utf-8') as f: content = f.read() port = re.findall(rf'{parameter_name}="(.*?)"\n', content, re.S) if len(port) > 0: logger.info(f"Match to dynamic port: {port[0]}") serial = f"127.0.0.1:{port[0]}" else: logger.warning(f"Did not match the result: {serial}.") break CloseKey(bs_keys) CloseKey(reg_root) return serial @cached_property def adb_binary(self): # Try adb in deploy.yaml config = poor_yaml_read(DEPLOY_CONFIG) if 'AdbExecutable' in config: file = config['AdbExecutable'].replace('\\', '/') if os.path.exists(file): return os.path.abspath(file) # Try existing adb.exe for file in self.adb_binary_list: if os.path.exists(file): return os.path.abspath(file) # Use adb.exe in system PATH file = 'adb.exe' return file def adb_command(self, cmd, timeout=10): """ Execute ADB commands in a subprocess, usually to be used when pulling or pushing large files. Args: cmd (list): timeout (int): Returns: str: """ cmd = list(map(str, cmd)) cmd = [self.adb_binary, '-s', self.serial] + cmd # Use shell=True to disable console window when using GUI. # Although, there's still a window when you stop running in GUI, which cause by gooey. # To disable it, edit gooey/gui/util/taskkill.py # No gooey anymore, just shell=False process = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=False) return process.communicate(timeout=timeout)[0] def adb_shell(self, cmd, **kwargs): """ Equivalent to `adb -s <serial> shell <*cmd>` Args: cmd (list): **kwargs: rstrip (bool): strip the last empty line (Default: True) stream (bool): return stream instead of string output (Default: False) Returns: str or socket if stream=True """ cmd = list(map(str, cmd)) result = self.adb.shell(cmd, timeout=10, **kwargs) return result @cached_property def reverse_server(self): """ Setup a server on Alas, access it from emulator. This will bypass adb shell and be faster. """ server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self._server_port = self.adb_reverse(f'tcp:{self.config.REVERSE_SERVER_PORT}') server.bind(('127.0.0.1', self._server_port)) server.listen(5) logger.info(f'Reverse server listening on {self._server_port}') return server def adb_shell_nc(self, cmd, timeout=5, chunk_size=262144): """ Args: cmd (list): timeout (int): chunk_size (int): Default to 262144 Returns: bytes: """ # <command> | nc 127.0.0.1 {port} cmd += ['|', 'nc', '127.0.0.1', self.config.REVERSE_SERVER_PORT] # Server start listening server = self.reverse_server server.settimeout(timeout) # Client send data, waiting for server accept _ = self.adb_shell(cmd, stream=True) try: # Server accept connection conn, conn_port = server.accept() except socket.timeout: raise AdbTimeout('reverse server accept timeout') # Server receive data data = recv_all(conn, chunk_size=chunk_size) # Server close connection conn.close() return data def adb_exec_out(self, cmd, serial=None): cmd.insert(0, 'exec-out') return self.adb_command(cmd, serial) def adb_forward(self, remote): """ Do `adb forward <local> <remote>`. choose a random port in FORWARD_PORT_RANGE or reuse an existing forward, and also remove redundant forwards. Args: remote (str): tcp:<port> localabstract:<unix domain socket name> localreserved:<unix domain socket name> localfilesystem:<unix domain socket name> dev:<character device name> jdwp:<process pid> (remote only) Returns: int: Port """ port = 0 for forward in self.adb.forward_list(): if forward.serial == self.serial and forward.remote == remote and forward.local.startswith('tcp:'): if not port: logger.info(f'Reuse forward: {forward}') port = int(forward.local[4:]) else: logger.info(f'Remove redundant forward: {forward}') self.adb_forward_remove(forward.local) if port: return port else: # Create new forward port = random_port(self.config.FORWARD_PORT_RANGE) forward = ForwardItem(self.serial, f'tcp:{port}', remote) logger.info(f'Create forward: {forward}') self.adb.forward(forward.local, forward.remote) return port def adb_reverse(self, remote): port = 0 for reverse in self.adb.reverse_list(): if reverse.remote == remote and reverse.local.startswith('tcp:'): if not port: logger.info(f'Reuse reverse: {reverse}') port = int(reverse.local[4:]) else: logger.info(f'Remove redundant forward: {reverse}') self.adb_forward_remove(reverse.local) if port: return port else: # Create new reverse port = random_port(self.config.FORWARD_PORT_RANGE) reverse = ReverseItem(f'tcp:{port}', remote) logger.info(f'Create reverse: {reverse}') self.adb.reverse(reverse.local, reverse.remote) return port def adb_forward_remove(self, local): """ Equivalent to `adb -s <serial> forward --remove <local>` More about the commands send to ADB server, see: https://cs.android.com/android/platform/superproject/+/master:packages/modules/adb/SERVICES.TXT Args: local (str): Such as 'tcp:2437' """ with self.adb_client._connect() as c: list_cmd = f"host-serial:{self.serial}:killforward:{local}" c.send_command(list_cmd) c.check_okay() def adb_reverse_remove(self, local): """ Equivalent to `adb -s <serial> reverse --remove <local>` Args: local (str): Such as 'tcp:2437' """ with self.adb_client._connect() as c: c.send_command(f"host:transport:{self.serial}") c.check_okay() list_cmd = f"reverse:killforward:{local}" c.send_command(list_cmd) c.check_okay() def adb_push(self, local, remote): """ Args: local (str): remote (str): Returns: str: """ cmd = ['push', local, remote] return self.adb_command(cmd) def adb_connect(self, serial): """ Connect to a serial, try 3 times at max. If there's an old ADB server running while Alas is using a newer one, which happens on Chinese emulators, the first connection is used to kill the other one, and the second is the real connect. Args: serial (str): Returns: bool: If success """ if 'emulator' in serial: return True else: for _ in range(3): msg = self.adb_client.connect(serial) logger.info(msg) if 'connected' in msg: # Connected to 127.0.0.1:59865 # Already connected to 127.0.0.1:59865 return True elif 'bad port' in msg: # bad port number '598265' in '127.0.0.1:598265' logger.error(msg) possible_reasons('Serial incorrect, might be a typo') raise RequestHumanTakeover logger.warning(f'Failed to connect {serial} after 3 trial, assume connected') return False def adb_disconnect(self, serial): msg = self.adb_client.disconnect(serial) if msg: logger.info(msg) del_cached_property(self, 'hermit_session') del_cached_property(self, 'minitouch_builder') del_cached_property(self, 'reverse_server') def install_uiautomator2(self): """ Init uiautomator2 and remove minicap. """ logger.info('Install uiautomator2') init = u2.init.Initer(self.adb, loglevel=logging.DEBUG) init.set_atx_agent_addr('127.0.0.1:7912') init.install() self.uninstall_minicap() def uninstall_minicap(self): """ minicap can't work or will send compressed images on some emulators. """ logger.info('Removing minicap') self.adb_shell(["rm", "/data/local/tmp/minicap"]) self.adb_shell(["rm", "/data/local/tmp/minicap.so"]) def restart_atx(self): """ Minitouch supports only one connection at a time. Restart ATX to kick the existing one. """ logger.info('Restart ATX') atx_agent_path = '/data/local/tmp/atx-agent' self.adb_shell([atx_agent_path, 'server', '--stop']) self.adb_shell([atx_agent_path, 'server', '--nouia', '-d', '--addr', '127.0.0.1:7912']) @staticmethod def sleep(second): """ Args: second(int, float, tuple): """ time.sleep(ensure_time(second)) _orientation_description = { 0: 'Normal', 1: 'HOME key on the right', 2: 'HOME key on the top', 3: 'HOME key on the left', } orientation = 0 def get_orientation(self): """ Rotation of the phone Returns: int: 0: 'Normal' 1: 'HOME key on the right' 2: 'HOME key on the top' 3: 'HOME key on the left' """ _DISPLAY_RE = re.compile( r'.*DisplayViewport{valid=true, .*orientation=(?P<orientation>\d+), .*deviceWidth=(?P<width>\d+), deviceHeight=(?P<height>\d+).*' ) output = self.adb_shell(['dumpsys', 'display']) res = _DISPLAY_RE.search(output, 0) if res: o = int(res.group('orientation')) if o in Connection._orientation_description: pass else: o = 0 logger.warning(f'Invalid device orientation: {o}, assume it is normal') else: o = 0 logger.warning('Unable to get device orientation, assume it is normal') self.orientation = o logger.attr('Device Orientation', f'{o} ({Connection._orientation_description.get(o, "Unknown")})') return o