def __init__(self, device_serial=None, is_emulator=True, output_dir=None, grant_perm=False, telnet_auth_token=None): """ initialize a device connection :param device_serial: serial number of target device :param is_emulator: boolean, type of device, True for emulator, False for real device :return: """ self.logger = logging.getLogger(self.__class__.__name__) if device_serial is None: import utils all_devices = utils.get_available_devices() if len(all_devices) == 0: self.logger.warning("ERROR: No device connected.") sys.exit(-1) device_serial = all_devices[0] self.serial = device_serial self.is_emulator = is_emulator self.output_dir = output_dir if output_dir is not None: if not os.path.isdir(output_dir): os.mkdir(output_dir) self.grant_perm = grant_perm # basic device information self.settings = {} self.display_info = None self.model_number = None self.sdk_version = None self.release_version = None self.ro_debuggable = None self.ro_secure = None self.connected = True self.last_know_state = None self.__used_ports = [] self.pause_sending_event = False # adapters self.adb = ADB(device=self) self.telnet = TelnetConsole(device=self, auth_token=telnet_auth_token) self.droidbot_app = DroidBotAppConn(device=self) self.minicap = Minicap(device=self) self.logcat = Logcat(device=self) self.user_input_monitor = UserInputMonitor(device=self) self.process_monitor = ProcessMonitor(device=self) self.droidbot_ime = DroidBotIme(device=self) self.adapters = { self.adb: True, self.telnet: False, self.droidbot_app: True, self.minicap: True, self.logcat: True, self.user_input_monitor: True, self.process_monitor: True, self.droidbot_ime: True }
def get_adb(self): """ get adb connection of the device """ if self.adb_enabled and self.adb is None: from adapter.adb import ADB self.adb = ADB(self) return self.adb
class TestADB(unittest.TestCase): def setUp(self): self.device = Device("emulator-5554") self.adb = ADB(self.device) def test_connect(self): self.assertTrue(self.adb.check_connectivity()) def test_run_cmd(self): r = self.adb.run_cmd(['get-state']) self.assertTrue(r.startswith('device')) r = self.adb.run_cmd("get-state") self.assertTrue(r.startswith('device')) def tearDown(self): self.adb.disconnect() self.device.disconnect()
class Device(object): """ this class describes a connected device """ def __init__(self, device_serial=None, is_emulator=True, output_dir=None, use_hierarchy_viewer=False, grant_perm=False, telnet_auth_token=None): """ initialize a device connection :param device_serial: serial number of target device :param is_emulator: boolean, type of device, True for emulator, False for real device :return: """ self.logger = logging.getLogger("Device") # options if device_serial is None: import utils all_devices = utils.get_available_devices() if len(all_devices) == 0: self.logger.warning("ERROR: No device connected.") sys.exit(-1) device_serial = all_devices[0] self.serial = device_serial self.is_emulator = is_emulator self.output_dir = output_dir if output_dir is not None: if not os.path.isdir(output_dir): os.mkdir(output_dir) self.grant_perm = grant_perm # basic device information self.settings = {} self.display_info = None self.sdk_version = None self.release_version = None self.ro_debuggable = None self.ro_secure = None self.is_connected = False # adapters self.adb = ADB(device=self) self.telnet = TelnetConsole(device=self, auth_token=telnet_auth_token) self.view_client = ViewClient(device=self, forceviewserveruse=use_hierarchy_viewer) self.droidbot_app = DroidBotAppConn(device=self) self.minicap = Minicap(device=self) self.logcat = Logcat(device=self) self.user_input_monitor = UserInputMonitor(device=self) self.process_monitor = ProcessMonitor(device=self) self.adapters = { self.adb: True, self.telnet: False, self.view_client: False, self.droidbot_app: True, self.minicap: True, self.logcat: True, self.user_input_monitor: True, self.process_monitor: True } # if self.is_emulator: # self.telnet_enabled = True def check_connectivity(self): """ check if the device is available """ for adapter in self.adapters: adapter_name = adapter.__class__.__name__ adapter_enabled = self.adapters[adapter] if not adapter_enabled: print "[CONNECTIVITY] %s is not enabled." % adapter_name else: if adapter.check_connectivity(): print "[CONNECTIVITY] %s is enabled and connected." % adapter_name else: print "[CONNECTIVITY] %s is enabled but not connected." % adapter_name def wait_for_device(self): """ wait until the device is fully booted :return: """ self.logger.info("waiting for device") try: subprocess.check_call( ["adb", "-s", self.serial, "wait-for-device"]) while True: out = subprocess.check_output([ "adb", "-s", self.serial, "shell", "getprop", "init.svc.bootanim" ]).split()[0] if out == "stopped": break time.sleep(3) except: self.logger.warning("error waiting for device") def set_up(self): """ Set connections on this device :return: """ # wait for emulator to start self.wait_for_device() for adapter in self.adapters: adapter_enabled = self.adapters[adapter] if not adapter_enabled: continue adapter.set_up() def connect(self): """ establish connections on this device :return: """ for adapter in self.adapters: adapter_enabled = self.adapters[adapter] if not adapter_enabled: continue adapter.connect() self.get_sdk_version() self.get_release_version() self.get_ro_secure() self.get_ro_debuggable() self.get_display_info() self.unlock() self.check_connectivity() self.is_connected = True def disconnect(self): """ disconnect current device :return: """ self.is_connected = False for adapter in self.adapters: adapter_enabled = self.adapters[adapter] if not adapter_enabled: continue adapter.disconnect() if self.output_dir is not None: temp_dir = os.path.join(self.output_dir, "temp") if os.path.exists(temp_dir): import shutil shutil.rmtree(temp_dir) def tear_down(self): for adapter in self.adapters: adapter_enabled = self.adapters[adapter] if not adapter_enabled: continue adapter.tear_down() def is_foreground(self, app): """ check if app is in foreground of device :param app: App :return: boolean """ if isinstance(app, str): package_name = app elif isinstance(app, App): package_name = app.get_package_name() else: return False top_activity_name = self.get_top_activity_name() if top_activity_name is None: return False return top_activity_name.startswith(package_name) def get_sdk_version(self): """ Get version of current SDK """ if self.sdk_version is None: self.sdk_version = self.adb.get_sdk_version() return self.sdk_version def get_release_version(self): """ Get version of current SDK """ if self.release_version is None: self.release_version = self.adb.get_release_version() return self.release_version def get_ro_secure(self): if self.ro_secure is None: self.ro_secure = self.adb.get_ro_secure() return self.ro_secure def get_ro_debuggable(self): if self.ro_debuggable is None: self.ro_debuggable = self.adb.get_ro_debuggable() return self.ro_debuggable def get_display_info(self, refresh=True): """ get device display infomation, including width, height, and density :param refresh: if set to True, refresh the display info instead of using the old values :return: dict, display_info """ if self.display_info is None or refresh: self.display_info = self.adb.get_display_info() return self.display_info def get_width(self, refresh=False): display_info = self.get_display_info(refresh=refresh) width = 0 if "width" in display_info: width = display_info["width"] elif not refresh: width = self.get_width(refresh=True) else: self.logger.warning("get_width: width not in display_info") return width def get_height(self, refresh=False): display_info = self.get_display_info(refresh=refresh) height = 0 if "height" in display_info: height = display_info["height"] elif not refresh: height = self.get_width(refresh=True) else: self.logger.warning("get_height: height not in display_info") return height def unlock(self): """ unlock screen skip first-use tutorials etc :return: """ self.adb.unlock() def shake(self): """ shake the device :return: """ telnet = self.telnet if telnet is None: self.logger.warning( "Telnet not connected, so can't shake the device.") l2h = range(0, 11) h2l = range(0, 11) h2l.reverse() sensor_xyz = [(-float(v * 10) + 1, float(v) + 9.8, float(v * 2) + 0.5) for v in [1, -1, 1, -1, 1, -1, 0]] for (x, y, z) in sensor_xyz: telnet.run_cmd("sensor set acceleration %f:%f:%f" % (x, y, z)) def add_env(self, env): """ set env to the device :param env: instance of AppEnv """ self.logger.info("deploying env: %s" % env) env.deploy(self) def add_contact(self, contact_data): """ add a contact to device :param contact_data: dict of contact, should have keys like name, phone, email :return: """ assert self.adb is not None contact_intent = Intent(prefix="start", action="android.intent.action.INSERT", mime_type="vnd.android.cursor.dir/contact", extra_string=contact_data) self.send_intent(intent=contact_intent) time.sleep(2) self.adb.press("BACK") time.sleep(2) self.adb.press("BACK") return True def receive_call(self, phone=DEFAULT_NUM): """ simulate a income phonecall :param phone: str, phonenum :return: """ assert self.telnet is not None return self.telnet.run_cmd("gsm call %s" % phone) def cancel_call(self, phone=DEFAULT_NUM): """ cancel phonecall :param phone: str, phonenum :return: """ assert self.telnet is not None return self.telnet.run_cmd("gsm cancel %s" % phone) def accept_call(self, phone=DEFAULT_NUM): """ accept phonecall :param phone: str, phonenum :return: """ assert self.telnet is not None return self.telnet.run_cmd("gsm accept %s" % phone) def call(self, phone=DEFAULT_NUM): """ simulate a outcome phonecall :param phone: str, phonenum :return: """ call_intent = Intent(prefix='start', action="android.intent.action.CALL", data_uri="tel:%s" % phone) return self.send_intent(intent=call_intent) def send_sms(self, phone=DEFAULT_NUM, content=DEFAULT_CONTENT): """ send a SMS :param phone: str, phone number of receiver :param content: str, content of sms :return: """ send_sms_intent = Intent(prefix='start', action="android.intent.action.SENDTO", data_uri="sms:%s" % phone, extra_string={'sms_body': content}, extra_boolean={'exit_on_sent': 'true'}) self.send_intent(intent=send_sms_intent) time.sleep(2) self.adb.press('66') return True def receive_sms(self, phone=DEFAULT_NUM, content=DEFAULT_CONTENT): """ receive a SMS :param phone: str, phone number of sender :param content: str, content of sms :return: """ assert self.telnet is not None return self.telnet.run_cmd("sms send %s '%s'" % (phone, content)) def set_gps(self, x, y): """ set GPS positioning to x,y :param x: float :param y: float :return: """ assert self.telnet is not None return self.telnet.run_cmd("geo fix %s %s" % (x, y)) def set_continuous_gps(self, center_x, center_y, delta_x, delta_y): import threading gps_thread = threading.Thread(target=self.set_continuous_gps_blocked, args=(center_x, center_y, delta_x, delta_y)) gps_thread.start() return True def set_continuous_gps_blocked(self, center_x, center_y, delta_x, delta_y): """ simulate GPS on device via telnet this method is blocked @param center_x: x coordinate of GPS position @param center_y: y coordinate of GPS position @param delta_x: range of x coordinate @param delta_y: range of y coordinate """ import random while self.is_connected: x = random.random() * delta_x * 2 + center_x - delta_x y = random.random() * delta_y * 2 + center_y - delta_y self.set_gps(x, y) time.sleep(3) def get_settings(self): """ get device settings via adb """ db_name = "/data/data/com.android.providers.settings/databases/settings.db" system_settings = {} out = self.adb.shell("sqlite3 %s \"select * from %s\"" % (db_name, "system")) out_lines = out.splitlines() for line in out_lines: segs = line.split('|') if len(segs) != 3: continue system_settings[segs[1]] = segs[2] secure_settings = {} out = self.adb.shell("sqlite3 %s \"select * from %s\"" % (db_name, "secure")) out_lines = out.splitlines() for line in out_lines: segs = line.split('|') if len(segs) != 3: continue secure_settings[segs[1]] = segs[2] self.settings['system'] = system_settings self.settings['secure'] = secure_settings return self.settings def change_settings(self, table_name, name, value): """ dangerous method, by calling this, change settings.db in device be very careful for sql injection :param table_name: table name to work on, usually it is system or secure :param name: settings name to set :param value: settings value to set """ db_name = "/data/data/com.android.providers.settings/databases/settings.db" self.adb.shell( "sqlite3 %s \"update '%s' set value='%s' where name='%s'\"" % (db_name, table_name, value, name)) return True def send_intent(self, intent): """ send an intent to device via am (ActivityManager) :param intent: instance of Intent :return: """ assert self.adb is not None assert intent is not None if isinstance(intent, Intent): cmd = intent.get_cmd() else: cmd = intent return self.adb.shell(cmd) def send_event(self, event): """ send one event to device :param event: the event to be sent :return: """ self.logger.info("sending event: %s" % event) event.send(self) def start_app(self, app): """ start an app on the device :param app: instance of App, or str of package name :return: """ if isinstance(app, str): package_name = app elif isinstance(app, App): package_name = app.get_package_name() if app.get_main_activity(): package_name += "/%s" % app.get_main_activity() else: self.logger.warning("unsupported param " + app + " with type: ", type(app)) return intent = Intent(suffix=package_name) self.send_intent(intent) def get_top_activity_name(self): """ Get current activity """ data = self.adb.shell("dumpsys activity top").splitlines() regex = re.compile("\s*ACTIVITY ([A-Za-z0-9_.]+)/([A-Za-z0-9_.]+)") m = regex.search(data[1]) if m: return m.group(1) + "/" + m.group(2) return None def get_task_activities(self): """ Get current tasks and corresponding activities. :return: a dict with three attributes: task_to_activities, current_task, and top_activity. task_to_activities is a dict mapping a task id to a list of activities, from top to down. current_task is the id of the active task. top_activity is the name of the top activity """ lines = self.adb.shell("dumpsys activity activities").splitlines() result = {} task_to_activities = {} activity_line_re = re.compile( '\* Hist #\d+: ActivityRecord{[^ ]+ [^ ]+ ([^ ]+) t(\d+)}') focused_activity_line_re = re.compile( 'mFocusedActivity: ActivityRecord{[^ ]+ [^ ]+ ([^ ]+) t(\d+)}') for line in lines: line = line.strip() if line.startswith("Task id #"): task_id = line[9:] task_to_activities[task_id] = [] elif line.startswith("* Hist #"): m = activity_line_re.match(line) if m: activity = m.group(1) task_id = m.group(2) if task_id not in task_to_activities: task_to_activities[task_id] = [] task_to_activities[task_id].append(activity) elif line.startswith("mFocusedActivity: "): m = focused_activity_line_re.match(line) if m: activity = m.group(1) task_id = m.group(2) result['current_task'] = task_id result['top_activity'] = activity result['task_to_activities'] = task_to_activities return result def get_service_names(self): """ get current running services :return: list of services """ services = [] dat = self.adb.shell('dumpsys activity services') lines = dat.splitlines() service_re = re.compile( '^.+ServiceRecord{.+ ([A-Za-z0-9_.]+)/([A-Za-z0-9_.]+)}') for line in lines: m = service_re.search(line) if m: package = m.group(1) service = m.group(2) services.append("%s/%s" % (package, service)) return services def get_focused_window_name(self): return self.adb.get_focused_window_name() def get_package_path(self, package_name): """ get installation path of a package (app) :param package_name: :return: package path of app in device """ dat = self.adb.shell('pm path %s' % package_name) package_path_re = re.compile('^package:(.+)$') m = package_path_re.match(dat) if m: path = m.group(1) return path.strip() return None def start_activity_via_monkey(self, package): """ use monkey to start activity @param package: package name of target activity """ cmd = 'monkey' if package: cmd += ' -p %s' % package out = self.adb.shell(cmd) if re.search(r"(Error)|(Cannot find 'App')", out, re.IGNORECASE | re.MULTILINE): raise RuntimeError(out) def install_app(self, app): """ install an app to device @param app: instance of App @return: """ assert isinstance(app, App) # subprocess.check_call(["adb", "-s", self.serial, "uninstall", app.get_package_name()], # stdout=subprocess.PIPE, stderr=subprocess.PIPE) install_cmd = ["adb", "-s", self.serial, "install", "-r"] if self.grant_perm: install_cmd.append("-g") install_cmd.append(app.app_path) subprocess.check_call(install_cmd, stdout=subprocess.PIPE) package_name = app.get_package_name() dumpsys_p = subprocess.Popen([ "adb", "-s", self.serial, "shell", "dumpsys", "package", package_name ], stdout=subprocess.PIPE) dumpsys_lines = [] while True: line = dumpsys_p.stdout.readline() if not line: break dumpsys_lines.append(line) main_activity = self.__parse_main_activity_from_dumpsys_lines( dumpsys_lines) self.logger.info("App installed: %s/%s" % (package_name, main_activity)) app.dumpsys_main_activity = main_activity if self.output_dir is not None: package_info_file_name = "%s/dumpsys_package_%s.txt" % ( self.output_dir, app.get_package_name()) package_info_file = open(package_info_file_name, "w") package_info_file.writelines(dumpsys_lines) package_info_file.close() @staticmethod def __parse_main_activity_from_dumpsys_lines(lines): main_activity = None activity_line_re = re.compile("[^ ]+ ([^ ]+)/([^ ]+) filter [^ ]+") action_re = re.compile("Action: \"([^ ]+)\"") category_re = re.compile("Category: \"([^ ]+)\"") activities = {} cur_package = None cur_activity = None cur_actions = [] cur_categories = [] for line in lines: line = line.strip() m = activity_line_re.match(line) if m: activities[cur_activity] = { "actions": cur_actions, "categories": cur_categories } cur_package = m.group(1) cur_activity = m.group(2) if cur_activity.startswith("."): cur_activity = cur_package + cur_activity cur_actions = [] cur_categories = [] else: m1 = action_re.match(line) if m1: cur_actions.append(m1.group(1)) else: m2 = category_re.match(line) if m2: cur_categories.append(m2.group(1)) if cur_activity is not None: activities[cur_activity] = { "actions": cur_actions, "categories": cur_categories } for activity in activities: if "android.intent.action.MAIN" in activities[activity]["actions"] \ and "android.intent.category.LAUNCHER" in activities[activity]["categories"]: main_activity = activity return main_activity def uninstall_app(self, app): """ Uninstall an app from device. :param app: an instance of App or a package name :return: """ if isinstance(app, App): package_name = app.get_package_name() else: package_name = app if package_name in self.adb.get_installed_apps(): subprocess.check_call( ["adb", "-s", self.serial, "uninstall", package_name], stdout=subprocess.PIPE) def get_app_pid(self, app): if isinstance(app, App): package = app.get_package_name() else: package = app name2pid = {} ps_out = self.adb.shell(["ps", "-t"]) ps_out_lines = ps_out.splitlines() ps_out_head = ps_out_lines[0].split() if ps_out_head[1] != "PID" or ps_out_head[-1] != "NAME": self.logger.warning("ps command output format error: %s" % ps_out_head) for ps_out_line in ps_out_lines[1:]: segs = ps_out_line.split() if len(segs) < 4: continue pid = int(segs[1]) name = segs[-1] name2pid[name] = pid if package in name2pid: return name2pid[package] possible_pids = [] for name in name2pid: if name.startswith(package): possible_pids.append(name2pid[name]) if len(possible_pids) > 0: return min(possible_pids) return None def push_file(self, local_file, remote_dir="/sdcard/"): """ push file/directory to target_dir :param local_file: path to file/directory in host machine :param remote_dir: path to target directory in device :return: """ if not os.path.exists(local_file): self.logger.warning("push_file file does not exist: %s" % local_file) self.adb.run_cmd(["push", local_file, remote_dir]) def pull_file(self, remote_file, local_file): self.adb.run_cmd(["pull", remote_file, local_file]) def take_screenshot(self): # image = None # # received = self.adb.shell("screencap -p").replace("\r\n", "\n") # import StringIO # stream = StringIO.StringIO(received) # # try: # from PIL import Image # image = Image.open(stream) # except IOError as e: # self.logger.warning("exception in take_screenshot: %s" % e) # return image if self.output_dir is None: return None from datetime import datetime tag = datetime.now().strftime("%Y-%m-%d_%H%M%S") local_image_dir = os.path.join(self.output_dir, "temp") if not os.path.exists(local_image_dir): os.mkdir(local_image_dir) if self.minicap is not None: last_screen = self.minicap.last_screen if last_screen is not None: local_image_path = os.path.join(local_image_dir, "screen_%s.jpg" % tag) f = open(local_image_path, 'w') f.write(last_screen) f.close() return local_image_path local_image_path = os.path.join(local_image_dir, "screen_%s.png" % tag) remote_image_path = "/sdcard/screen_%s.png" % tag self.adb.shell("screencap -p %s" % remote_image_path) self.pull_file(remote_image_path, local_image_path) self.adb.shell("rm %s" % remote_image_path) return local_image_path def get_current_state(self): self.logger.info("getting current device state...") current_state = None try: view_client_views = self.get_views() foreground_activity = self.get_top_activity_name() background_services = self.get_service_names() screenshot_path = self.take_screenshot() self.logger.debug("finish getting current device state...") from device_state import DeviceState current_state = DeviceState( self, view_client_views=view_client_views, foreground_activity=foreground_activity, background_services=background_services, screenshot_path=screenshot_path) except Exception as e: self.logger.warning("exception in get_current_state: %s" % e) import traceback traceback.print_exc() return current_state def view_touch(self, x, y): self.adb.touch(x, y) def view_long_touch(self, x, y, duration=2000): """ Long touches at (x, y) @param duration: duration in ms This workaround was suggested by U{HaMi<http://stackoverflow.com/users/2571957/hami>} """ self.adb.long_touch(x, y, duration) def view_drag(self, (x0, y0), (x1, y1), duration): """ Sends drag event n PX (actually it's using C{input swipe} command. @param (x0, y0): starting point in PX @param (x1, y1): ending point in PX @param duration: duration of the event in ms """ self.adb.drag((x0, y0), (x1, y1), duration)
def __init__(self, device_serial=None, is_emulator=False, output_dir=None, cv_mode=False, grant_perm=False, telnet_auth_token=None, enable_accessibility_hard=False, humanoid=None, ignore_ad=False): """ initialize a device connection :param device_serial: serial number of target device :param is_emulator: boolean, type of device, True for emulator, False for real device :return: """ self.logger = logging.getLogger(self.__class__.__name__) if device_serial is None: from utils import get_available_devices all_devices = get_available_devices() if len(all_devices) == 0: self.logger.warning("ERROR: No device connected.") sys.exit(-1) device_serial = all_devices[0] if "emulator" in device_serial and not is_emulator: self.logger.warning("Seems like you are using an emulator. If so, please add is_emulator option.") self.serial = device_serial self.is_emulator = is_emulator self.cv_mode = cv_mode self.output_dir = output_dir if output_dir is not None: if not os.path.isdir(output_dir): os.makedirs(output_dir) self.grant_perm = grant_perm self.enable_accessibility_hard = enable_accessibility_hard self.humanoid = humanoid self.ignore_ad = ignore_ad # basic device information self.settings = {} self.display_info = None self.model_number = None self.sdk_version = None self.release_version = None self.ro_debuggable = None self.ro_secure = None self.connected = True self.last_know_state = None self.__used_ports = [] self.pause_sending_event = False # adapters self.adb = ADB(device=self) self.telnet = TelnetConsole(device=self, auth_token=telnet_auth_token) self.droidbot_app = DroidBotAppConn(device=self) self.minicap = Minicap(device=self) self.logcat = Logcat(device=self) self.user_input_monitor = UserInputMonitor(device=self) self.process_monitor = ProcessMonitor(device=self) self.droidbot_ime = DroidBotIme(device=self) self.adapters = { self.adb: True, self.telnet: False, self.droidbot_app: True, self.minicap: True, self.logcat: True, self.user_input_monitor: True, self.process_monitor: True, self.droidbot_ime: True } # minicap currently not working on emulators if self.is_emulator: self.logger.info("disable minicap on emulator") self.adapters[self.minicap] = False
def setUp(self): self.device = Device("emulator-5554") self.adb = ADB(self.device)