Пример #1
0
 def __init__(self, id_or_adb_backend=None):
     '''获取一个Android设备,获取成功后则独占该设备。
     
     :param device_id: 获取制定设备id的Android设备。如果为空则任意获取一台空闲设备。
     '''
     self._device = None
     if isinstance(id_or_adb_backend, str):
         adb_backend = LocalADBBackend.open_device(id_or_adb_backend)
     elif not id_or_adb_backend:
         local_device_list = LocalDeviceProvider.list()
         if not local_device_list: raise RuntimeError('No local device found')
         adb_backend = LocalADBBackend.open_device(local_device_list[0])
     else:
         adb_backend = id_or_adb_backend
     self._adb = ADB.open_device(adb_backend)
     self._device_driver = DeviceDriver(self._adb)
     Device.device_list.append(self)
Пример #2
0
class Device(object):
    '''Android设备类
    '''
    device_list = [] 

    def __init__(self, id_or_adb_backend=None):
        '''获取一个Android设备,获取成功后则独占该设备。
        
        :param device_id: 获取制定设备id的Android设备。如果为空则任意获取一台空闲设备。
        '''
        self._device = None
        if isinstance(id_or_adb_backend, str):
            adb_backend = LocalADBBackend.open_device(id_or_adb_backend)
        elif not id_or_adb_backend:
            local_device_list = LocalDeviceProvider.list()
            if not local_device_list: raise RuntimeError('No local device found')
            adb_backend = LocalADBBackend.open_device(local_device_list[0])
        else:
            adb_backend = id_or_adb_backend
        self._adb = ADB.open_device(adb_backend)
        self._device_driver = DeviceDriver(self._adb)
        Device.device_list.append(self)

    def __del__(self):
        '释放设备'
        pass
        
    @staticmethod
    def release_all_device():
        '''释放所有设备
        '''
        qta_logger.info('释放设备资源')
        Device.device_list = []
        DeviceProviderManager().release_all()

    @static_property
    def device_id(self):
        '''设备ID
        '''
        return self.adb.device_name
    
    @property
    def device_host(self):
        '''设备主机
        '''
        return self.adb.device_host
        
    @static_property
    def cpu_type(self):
        '''cpu类型
        '''
        return self.adb.get_cpu_abi()

    @property
    def imei(self):
        '''手机串号
        '''
        if not hasattr(self, '_imei'):
            self._imei = self._device_driver.get_device_imei()
        return self._imei

    @static_property
    def model(self):
        '''设备型号
        '''
        return self.adb.get_device_model()

    @static_property
    def system_version(self):
        '''系统版本
        '''
        return self.adb.get_system_version()

    @static_property
    def sdk_version(self):
        '''SDK版本
        '''
        return self.adb.get_sdk_version()

    @property
    def screen_size(self):
        '''屏幕大小
        '''
        return self._device_driver.get_screen_size()
    
    @static_property
    def screen_scale(self):
        '''屏幕缩放比例
        '''
        if self.is_emulator_device():
            return int(self.adb.get_property('qemu.sf.lcd_density')) / 160.0
        else:
            return int(self.adb.get_property('ro.sf.lcd_density')) / 160.0
    
    @property
    def language(self):
        '''语言
        '''
        return self._device_driver.get_language()

    @property
    def country(self):
        '''国家
        '''
        return self._device_driver.get_country()
    
    @property
    def current_activity(self):
        '''当前Activity
        '''
        return self.get_current_activity()
    
    @property
    def adb(self):
        return self._adb
    
    @property
    def debuggable(self):
        '''是否是调试版系统
        '''
        return not (self.adb.get_property('ro.secure') == '1' and self.adb.get_property('ro.debuggable') == '0')

    def __str__(self):
        return '%s(%s %s Android %s)' % (self.device_id, self.model, self.cpu_type, self.system_version)
    
    def get_imei(self):
        '''获取设备imei号
        '''
        try:
            return self.adb.get_device_imei()
        except RuntimeError as e:
            logger.warn('Read device imei by dumpsys failed: %s' % e)
            return self._device_driver.get_device_imei()

    def is_rooted(self):
        '''是否root
        '''
        return self.adb.is_rooted()

    def is_emulator_device(self):
        '''是否是模拟器设备
        '''
        return self.adb.get_property('ro.kernel.android.qemud').strip() == '1'
        
    def run_shell_cmd(self, cmd, *args, **kwargs):
        '''执行adb shell命令
        '''
        return self.adb.run_shell_cmd(cmd, *args, **kwargs)
    
    def run_as(self, package_name, cmd, **kwargs):
        '''以package_name权限执行命令cmd
        
        :param package_name: 包名,必须是已经安装的debug包
        :type  package_name: string
        :param cmd:          命令行
        :type  cmd:          string
        '''
        return self.adb.run_as(package_name, cmd, **kwargs)
    
    def start_activity(self, activity_name, action='', type='', data_uri='', extra={}, wait=True):
        '''启动activity

        :param activity_name: Activity名称,如:com.tencent.mobileqq/.activity.SplashActivity
        :type  activity_name: string
        :param action:        Action名称
        :type  action:        string
        :param type:          mime类型
        :type  type:          string
        :param data_uri:      data uri
        :type  data_uri:      string
        :param extra:         额外参数
        :type  extra:         dict
        :param wait:          是否等待启动完成
        :type  wait:          boolean
        '''
        use_am = True

        for key in extra.keys():
            if isinstance(extra[key], (list, tuple)):
                use_am = False
                break
        if use_am: return self.adb.start_activity(activity_name, action, type, data_uri, extra, wait)
        
        params = {'Component': activity_name}
        if action: params['Action'] = action
        if type: params['Type'] = type
        if extra: params['FileUri'] = extra['android.intent.extra.STREAM']
        return self._device_driver.start_activity(params)

    def install_package(self, pkg_path, pkg_name='', overwrite=False):
        '''安装应用
        
        :param pkg_path:  安装包路径
        :type  pkg_path:  string
        :param pkg_name:  应用包名
        :type  pkg_name:  string
        :param overwrite: 是否是覆盖安装
        :type  overwrite: bool
        '''
        return self._device_driver.install_package(pkg_path, overwrite)
    
    def uninstall_package(self, pkg_name):
        '''卸载应用
        
        :param pkg_name: 包名
        :type  pkg_name: string
        '''
        return self.adb.uninstall_app(pkg_name)
        
    def kill_process(self, package_name):
        '''杀死进程
        
        :param package_name: 应用包名
        :type package_name: string
        '''
        return self._device_driver.kill_process(package_name)

    def push_file(self, src_path, dst_path):
        '''向手机中拷贝文件
        
        :param src_path: PC上的源路径
        :type src_path:  string
        :param dst_path: 手机上的目标路径
        :type dst_path:  string
        '''
        
        if not os.path.exists(src_path):
            raise RuntimeError('File: %s not exist' % src_path)
        file_size = os.path.getsize(src_path)
        is_zip = False
        if file_size >= 5 * 1024 * 1024:
            is_zip = True
            zip_file_path = src_path + '.zip'
            zip_file = zipfile.ZipFile(zip_file_path, 'w', zipfile.ZIP_DEFLATED)
            if dst_path[-1] == '/':
                # filename not specified
                filename = os.path.split(src_path)[-1]
            else:
                filename = dst_path.split('/')[-1]
            zip_file.write(src_path, filename)
            zip_file.close()
            src_path = zip_file_path
            dst_path += '.zip'
        ret = self.adb.push_file(src_path, dst_path)
        if is_zip:
            os.remove(src_path)
            if not self._device_driver.unzip_file(dst_path, dst_path[:dst_path.rfind('/')]):
                logger.warn('unzip file %s failed' % dst_path)
                ret = self.adb.push_file(src_path[:-4], dst_path[:-4])
            elif dst_path.startswith('/data/'):
                self.adb.chmod(dst_path[:-4], '744')
            self.delete_file(dst_path)
            dst_path = dst_path[:-4]
        try:
            self.run_shell_cmd('touch "%s"' % dst_path)  # 修改文件修改时间
        except:
            logger.exception('touch file %s error' % dst_path)
        return ret
    
    def push_dir(self, src_path, dst_path):
        '''向手机中拷贝文件夹
        
        :param src_path: PC上的源目录路径
        :type src_path:  string
        :param dst_path: 手机上的目的目录路径
        :type dst_path:  string
        '''
        if not os.path.exists(src_path):
            raise RuntimeError('Directory %s not exist' % src_path)
        for file in os.listdir(src_path):
            file_src_path = os.path.join(src_path, file)
            file_dst_path = dst_path + '/' + file
            self.push_file(file_src_path, file_dst_path)

    def pull_file(self, src_path, dst_path):
        '''将手机中的文件拷贝到PC中
        
        :param src_path: 手机中的源路径
        :type src_path:  string
        :param dst_path: PC中的目的路径
        :type dst_path:  string
        '''
        self.adb.list_dir(src_path)
        try:
            ret = self.adb.pull_file(src_path, dst_path)
            if 'does not exist' not in ret: return
        except RuntimeError as e:
            logger.warn('pull file failed: %r' % e)
            if src_path.startswith('/data/local/tmp/'): raise e
        _, files = self.adb.list_dir(src_path)
        # 需要root权限
        tmp_path = '/data/local/tmp/%s' % files[0]['name']
        self.adb.copy_file(src_path, tmp_path)
        self.adb.chmod(tmp_path, 444)
        self.pull_file(tmp_path, dst_path)
        self.adb.delete_file(tmp_path)

    def pull_dir(self, src_path, dst_path):
        '''从手机中拷贝文件夹到PC
        
        :param src_path: 手机上的源目录路径
        :type src_path:  string
        :param dst_path: PC上的目的目录路径
        :type dst_path:  string
        '''
        if not os.path.exists(dst_path):
            os.mkdir(dst_path)
        subdirs, files = self.adb.list_dir(src_path)
        for file in files:
            self.pull_file(src_path + '/' + file['name'], os.path.join(dst_path, file['name']))
        for subdir in subdirs:
            self.pull_dir(src_path + '/' + subdir['name'], os.path.join(dst_path, subdir['name']))

    def list_dir(self, dir_path):
        '''列取目录
        '''
        return self.adb.list_dir(dir_path)
    
    def is_file_exists(self, file_path):
        '''判断文件或目录是否存在

        :param file_path: 文件或目录在设备中的路径
        :type  file_path: string
        '''
        try:
            self.list_dir(file_path)
            return True
        except RuntimeError:
            return False
        
    def delete_folder(self, folder_path):
        '''删除文件夹
        
        :param folder_path: 手机中的文件夹路径
        :type folder_path:  string
        '''
        return self.adb.delete_folder(folder_path)

    def delete_file(self, file_path):
        '''删除文件

        :param file_path: 文件在设备中的路径
        :type  file_path: string
        '''
        return self.adb.delete_file(file_path)

    def mkdir(self, dir_path):
        '''创建目录

        :param dir_path: 要创建的目录路径
        :type  dir_path: string
        '''
        return self.adb.mkdir(dir_path)
    
    def copy_file(self, src_path, dst_path):
        '''设备内复制文件
        
        :param src_path: 源路径
        :type src_path:  string
        :param dst_path: 目标路径
        :type dst_path:  string
        '''
        if not self.is_file_exists(src_path):
            raise RuntimeError('File %s not exist' % src_path)
        self.adb.copy_file(src_path, dst_path)
        
    def get_external_sdcard_path(self):
        '''获取外置SD卡路径
        '''
        return self._device_driver.get_external_sdcard_path()

    def refresh_media_store(self, file_path=''):
        '''刷新图库,显示新拷贝到手机的图片

        :param file_path: 要刷新的图片路径,不指定则刷新整个sdcard
        :type  file_path: string
        '''
        return self._device_driver.refresh_media_store(file_path)

    def get_current_activity(self):
        '''获取当前Activtiy
        '''
        return self._device_driver.get_current_activity()

    def take_screen_shot(self, save_path):
        '''截屏
        
        :param save_path: 截屏图片存放在PC上的路径
        :type save_path: string
        '''
        return self._device_driver.take_screen_shot(save_path)
    
    def record_screen(self, save_path, record_time, frame_rate=10, quality=20):
        '''录屏
        
        :param save_path:   保存路径,如果为已存在的目录路径,则会将每一帧图片保存到该目录下
        :type  save_path:   string
        :param record_time: 录制时间,单位:秒
        :type  record-time: int/float
        :param frame_rate:  帧率,1-30
        :type  frame_rate:  int
        :param quality:     压缩质量,10-100
        :type  quality:     int
        '''
        import shutil
        from qt4a.androiddriver.device_driver import qt4a_path
        to_video = True
        if os.path.exists(save_path) and os.path.isdir(save_path): to_video = False
        
        if frame_rate < 1 or frame_rate > 30:
            raise ValueError('frame rate must between 1 and 30')
        if quality < 10 or quality > 100:
            raise ValueError('quality must between 10 and 100')
        remote_tmp_path = '%s/screen.record' % qt4a_path
        self.run_shell_cmd('%s/screenshot record -p %s -t %d -f %d -q %d' % (qt4a_path, remote_tmp_path, int(record_time * 1000), int(frame_rate), int(quality)))
        local_tmp_path = tempfile.mktemp()
        self.pull_file(remote_tmp_path, local_tmp_path)
        save_dir = save_path
        if to_video: save_dir = tempfile.mkdtemp('.screenshot')
        frame_list = Device.extract_record_frame(local_tmp_path, save_dir)
        os.remove(local_tmp_path)
        if to_video:
            Device.screen_frame_to_video(frame_list, frame_rate, save_path)
            shutil.rmtree(save_dir)
        result = []
        for it in os.listdir(save_dir):
            result.append(os.path.join(save_dir, it))
        return result
    
    @staticmethod
    def screen_frame_to_video(frame_list, frame_rate, save_path):
        '''将录屏帧序列转换为视频文件
        '''
        try:
            import cv2
        except ImportError:
            return None
        
        _, width, height = cv2.imread(frame_list[0]).shape[::-1]
        format = 'MJPG'
        if save_path.lower().endswith('.flv'): format = 'FLV1'
        elif save_path.lower().endswith('.mp4'): format = 'DIVX'
        videoWriter = cv2.VideoWriter(save_path, cv2.VideoWriter_fourcc(*format), frame_rate, (width, height))
        for it in frame_list:
            img = cv2.imread(it) 
            videoWriter.write(img)
        return save_path
            
    @staticmethod
    def extract_record_frame(file_path, save_dir):
        '''提取录屏文件中的帧
        '''
        frame_list = []
        with open(file_path, 'rb') as fp:
            while True:
                data = fp.read(4)
                if not data: break
                timestamp = struct.unpack('I', data)[0]
                data_len = struct.unpack('I', fp.read(4))[0]
                data = fp.read(data_len)
                save_path = os.path.join(save_dir, '%.8d.jpg' % timestamp)
                with open(save_path, 'wb') as f:
                    f.write(data)
                frame_list.append(save_path)
        return frame_list
    
    def drag(self, x1, y1, x2, y2, count=5, wait_time=40, send_down_event=True, send_up_event=True):
        '''滑动
        
        :param x1: 起始x坐标
        :type x1:  int
        :param y1: 起始y坐标
        :type y1:  int
        :param x2: 终止x坐标
        :type x2:  int
        :param y2: 终止y坐标
        :type y2:  int
        :param count: 滑动的步数
        :type count:  int
        :param wait_time: 每步间等待的时长
        :type wait_time:  int,单位:ms
        :param send_down_event: 是否发送DOWN事件
        :type  send_down_event: bool
        :param send_up_event: 是否发送UP事件
        :type  send_up_event: bool
        '''
        return self._device_driver.drag(x1, y1, x2, y2, count, wait_time, send_down_event, send_up_event)

    def click(self, x, y):
        '''单击屏幕坐标

        :param x: 横坐标
        :type  x: int/float
        :param y: 纵坐标
        :type  y: int/float
        '''
        return self._device_driver.drag(x, y, x, y)

    def long_click(self, x, y, duration=1):
        '''长按屏幕坐标

        :param x: 横坐标
        :type  x: int/float
        :param y: 纵坐标
        :type  y: int/float
        :param duration: 按住时长,单位为秒
        :type duration:  int/float
        '''
        self._device_driver.drag(x, y, x, y, send_up_event=False)
        time.sleep(duration)
        self._device_driver.drag(x, y, x, y, send_down_event=False)

    def reboot(self, wait_cpu_low=True, usage=20, duration=10, timeout=120):
        '''重启手机
        
        :param wait_cpu_low:   是否等待CPU使用率降低
        :type wait_cpu_low:    bool
        :param usage:          cpu使用率阈值
        :type  usage:          int
        :param duration:       持续时间(秒)
        :type  duration:       int
        :param timeout:        超时时间,超市时间到后,无论当前CPU使用率是多少,都会返回
        :type  timeout:        int
        '''
        return self._device_driver.reboot(wait_cpu_low, usage=usage, duration=duration, timeout=timeout)

    def connect_wifi(self, wifi_name, wifi_pass=''):
        '''连接指定的Wifi
        
        :param wifi_name: WiFi名称
        :type wifi_name:  string
        '''
        for _ in range(3):
            if self._device_driver.connect_wifi(wifi_name, wifi_pass): return True
            self.disable_wifi()  # 有些手机需要禁用一下wifi才能正常连接
            time.sleep(10)
        return False
    
    def enable_wifi(self):
        '''启用Wifi
        '''
        ssid = ''
        pwd = ''

        if hasattr(settings, 'QT4A_WIFI_SSID'):
            ssid = settings.QT4A_WIFI_SSID
        if hasattr(settings, 'QT4A_WIFI_PASSWORD'):
            pwd = settings.QT4A_WIFI_PASSWORD

        if ssid: return self.connect_wifi(ssid, pwd)

        return False

    def disable_wifi(self):
        '''禁用Wifi
        '''
        return self._device_driver.disable_wifi()
    
    def switch_to_data_connection(self):
        '''关闭WIFI,启用数据连接
        '''
        return self.disable_wifi() and self._device_driver.enable_data_connection()

    def disable_data_connection(self):
        '''禁用数据连接
        '''
        sim_state = self.get_sim_card_state()
        if 'SIM_STATE_ABSENT' in sim_state or 'SIM_STATE_UNKNOWN' in sim_state: return True
        return self._device_driver.disable_data_connection()

    def disable_network(self):
        '''禁用所有网络
        '''
        return self.disable_wifi() and self.disable_data_connection()

    def enable_network(self):
        '''启用任一网络,优先使用Wifi
        '''
        return self.enable_wifi() or self.enable_data_connection()

    def read_logcat(self, tag, process_name_pattern, pattern, num=1):
        '''查找最近满足条件的一条log
        
        :param tag: 期望的Tag
        :type tag:  string
        :param process_name_pattern: 期望的进程名,传入正则表达式
        :type process_name_pattern:  string
        :param pattern:  期望匹配的格式
        :type pattern:   Pattern
        :param num:  返回满足条件的日志条数
        :type num:   int
        '''
        from qt4a.androiddriver.util import logger
        pat = re.compile(r'^\[(.+)\(\d+\)\]\s+\[.+\]\s+\w/(.+)\(\s*\d+\):\s+(.+)$')
        log_pat = re.compile(pattern)
        log_list = self.adb.get_log(False)
        result_list = []
        k = 0
        for i in range(len(log_list) - 1, -1, -1):
            ret = pat.match(log_list[i])
            if not ret: 
                logger.info('read_logcat:%s not match ^\[(.+)\(\d+\)\]\s+\[.+\]\s+\w/(.+)\(\s*\d+\):\s+(.+)$' % log_list[i])
                continue
            if not process_name_pattern: continue
            process_pat = re.compile(process_name_pattern)
            if not process_pat.match(ret.group(1)): continue
            if ret.group(2) != tag: continue
            if log_pat.search(ret.group(3)):
                if num == 1:
                    return ret.group(3)
                elif num == 0 or k < num:
                    k += 1
                    result_list.append(ret.group(3))
                else:
                    break
        return result_list

    def get_clipboard_text(self):
        '''获取剪切板内容
        '''
        return self._device_driver.get_clipboard_text()
    
    def set_clipboard_text(self, text):
        '''设置剪贴板内容
        '''
        return self._device_driver.set_clipboard_text(text)
    
    def wake_screen(self, wake=True):
        '''唤醒屏幕
        '''
        return self._device_driver.wake_screen(wake)

    def unlock_keyguard(self):
        '''解锁屏幕
        '''
        return self._device_driver.unlock_keyguard()
    
    def lock_keyguard(self):
        '''锁屏
        '''
        return self._device_driver.lock_keyguard()
    
    def send_key(self, key):
        '''发送按键
        
        :param key: 按键
        :type key:  string
        '''
        return self._device_driver.send_key(key)
    
    def send_text(self, text):
        '''通过输入法方式发送文本
        
        :param text: 要发送的文本
        :type  text: string
        '''
        max_send_size = 960
        if isinstance(text, bytes):
            text = text.decode('utf8')
        text_en = ''
        i = 0
        while i < len(text):
            c = text[i]
            if c >= u'\ud800' and c <= u'\udbff':
                d = text[i + 1]
                if d >= u'\udc00' and d <= u'\udfff':
                    # 四字节unicode编码
                    text_en += r'\u%.4x\u%.4x' % (ord(c), ord(d))
                    i += 2
                    continue
            text_en += c
            i += 1
        
        text_en = text_en.encode('raw_unicode_escape')
        total_len = len(text_en)
        if total_len > max_send_size: raise RuntimeError('Text is too long %d' % total_len)
        extra = {'text': text_en}
        if len(text_en) == 1: extra['toClear'] = 'false'
        self.adb.send_broadcast('com.test.androidspy.input', extra)
        
    def clear_data(self, package_name):
        '''清理应用数据
        
        :param package_name: 包名
        :type package_name:  string
        '''
        if not self.adb.get_package_path(package_name): return True
        cmdline = 'pm clear %s' % package_name
        if self.adb.is_rooted():
            return 'Success' in self.run_shell_cmd(cmdline, True)
        else:
            result = self.run_shell_cmd(cmdline)
            if 'Success' in result:
                return True
            logger.warn('clear %s data failed: %s' % (package_name, result))
            return 'Success' in self.run_as(package_name, cmdline)

    def get_string_resource(self, pkg_name, string_id, lang=''):
        '''获取字符串资源
        '''
        result = self._device_driver.get_string_resource(pkg_name, string_id, lang)
        if result == ("Can't find string %s" % string_id) or 'Can\'t find class' in result:
            if pkg_name == 'android': raise RuntimeError(result)
            return self.get_string_resource('android', string_id, lang)
        else:
            return result
        
    def get_string_resource_id(self, pkg_name, text):
        '''获取字符串资源ID
        '''
        return self._device_driver.get_string_resource_id(pkg_name, text)

    def set_default_language(self, lang):
        '''设置默认语言
        '''
        return self._device_driver.set_default_language(lang)

    def is_app_installed(self, app_name):
        '''应用是否安装
        '''
        package_path = self.adb.get_package_path(app_name)
        return package_path != ''

    def get_static_field_value(self, pkg_name, cls_name, field_name, field_type=''):
        '''获取类中静态变量的值
        
        :param pkg_name:   包名
        :type pkg_name:    string
        :param cls_name:   类名
        :type cls_name:    string
        :param field_name: 字段名
        :type field_name:  string 
        '''
        return self._device_driver.get_static_field_value(pkg_name, cls_name, field_name, field_type)

    def set_default_app(self, action, type, new_app):
        '''设置默认应用
        
        :param action:  应用针对的类型,如:android.media.action.IMAGE_CAPTURE
        :type  action:  String
        :param new_app: 新的应用包名
        :type  new_app: String
        '''
        return self._device_driver.set_default_app(action, type, new_app)
    
    def set_camera_photo(self, pic_path):
        '''设置相机图片,调用该接口后,调用相机接口会返回指定图片
        
        :param pic_path: 图片在PC中的路径
        :type pic_path:  String
        '''
        if not os.path.exists(pic_path):
            raise RuntimeError('图片:%s 不存在' % pic_path)
        dst_path = '/sdcard/dcim/camera%s' % (os.path.splitext(pic_path)[1])
        self.push_file(pic_path, dst_path)
        self.adb.set_property('debug.mockcamera.image_path', dst_path)
        self.set_default_app('android.media.action.IMAGE_CAPTURE', '', 'com.test.androidspy')
    
    def clear_camera_default_app(self):
        '''清除默认相机应用
        '''
        from qt4a.androiddriver.util import logger
        if self.is_rooted():
            return self._device_driver.clear_default_app('android.media.action.IMAGE_CAPTURE')
        else:
            logger.warn('clear_camera_default_app need root')
           
    def has_gps(self):
        '''是否有GPS
        '''
        return self._device_driver.has_gps()
    
    def has_camera(self):
        '''是否有摄像头
        '''
        return self._device_driver.get_camera_number() > 0

    def get_sim_card_state(self):
        '''获取sim卡状态
        '''
        sim_state = self.adb.get_property('gsm.sim.state').strip()
        if sim_state == 'READY': return 'SIM_STATE_READY'
        if 'ABSENT' in sim_state or 'NOT_READY' in sim_state:
            return 'SIM_STATE_ABSENT'
        logger.info('sim state: %s' % sim_state)
        return 'SIM_STATE_UNKNOWN'
    
    def has_sim_card(self):
        '''是否有sim卡
        '''
        sim_state = self.get_sim_card_state()
        return sim_state != 'SIM_STATE_ABSENT' and sim_state != 'SIM_STATE_UNKNOWN'  # 虚拟机返回的状态是SIM_STATE_UNKNOWN
    
    def is_debug_package(self, package_name):
        '''是否是debug包
        '''
        return self._device_driver.is_debug_package(package_name)
    
    def _get_view_id(self, package_name, str_id):
        '''从控件字符串ID获取整型ID
        
        :param package_name: 应用包名
        :type package_name: string
        :param str_id: 字符串ID
        :type str_id:  string
        '''
        if not hasattr(self, '_view_id_dict'):
            self._view_id_dict = {}  # 该操作较为耗时,必须要缓存
        if not package_name in self._view_id_dict:
            self._view_id_dict[package_name] = {}
        if str_id in self._view_id_dict[package_name]:
            return self._view_id_dict[package_name][str_id]
        view_id = self._device_driver.get_view_id(package_name, str_id)
        self._view_id_dict[package_name][str_id] = view_id
        return view_id
    
    def _get_resource_origin_name(self, package_name, res_type, confuse_name):
        '''根据获取资源混淆后的名称获取原始名称
        
        :param package_name: 应用包名
        :type package_name:  string
        :param res_type:     资源类型
        :type res_type:      string
        :param confuse_name: 混淆后的名称
        :type confuse_name:  string
        '''
        if confuse_name == '': return ''  # 处理空字符串情况
        if len(confuse_name) > 4: return confuse_name  # 必然没有混淆
        if not hasattr(self, '_resource_name'):
            self._resource_name = {}  # 进行缓存
        if not package_name in self._resource_name:
            self._resource_name[package_name] = {}
        if not confuse_name in self._resource_name[package_name]:
            self._resource_name[package_name][confuse_name] = self._device_driver.get_resource_origin_name(package_name, res_type, confuse_name)
        return self._resource_name[package_name][confuse_name]
    
    def send_text_to_app(self, activity, text):
        '''向app分享文本
        '''
        return self.start_activity(activity, action='android.intent.action.SEND', type='text/plain', extra={'android.intent.extra.TEXT': text})
    
    def send_image_to_app(self, activity, image_path):
        '''向app分享图片
        '''
        def _copy_image(src_path):
            from qt4a.androiddriver.util import get_file_md5
            if six.PY2 and not isinstance(src_path, unicode): src_path = src_path.decode('utf8')
            file_ext = os.path.splitext(src_path)[-1]
            dst_path = '/sdcard/dcim/%s%s' % (get_file_md5(src_path), file_ext)
            self.push_file(src_path, dst_path)
            return dst_path
        
        action = 'android.intent.action.SEND'
        if isinstance(image_path, (list, tuple)):
            image_path_new = [None for _ in range(len(image_path))]
            for i in range(len(image_path)):
                image_path_new[i] = 'file://' + _copy_image(image_path[i])
            image_path = image_path_new
            action = 'android.intent.action.SEND_MULTIPLE'
        else:
            image_path = 'file://' + _copy_image(image_path)
        return self.start_activity(activity, action=action, type='image/*', extra={'android.intent.extra.STREAM': image_path})
    
    def send_file_to_app(self, activity, file_path):
        '''向app分享文件
        '''
        def _copy_file(src_path):
            if six.PY2 and not isinstance(src_path, unicode): src_path = src_path.decode('utf8')
            file_name = os.path.split(src_path)[-1]
            dst_path = '/data/local/tmp/%s' % (file_name)
            self.push_file(src_path, dst_path)
            return dst_path
        file_path = 'file://' + _copy_file(file_path)
        return self.start_activity(activity, action='android.intent.action.SEND', type='application/octet-stream', extra={'android.intent.extra.STREAM': file_path})
    
    def check_netstat(self):
        '''检查网络状态
        '''
        try:
            return 'bytes from' in self.run_shell_cmd('ping -c 1 www.qq.com', self.is_rooted())
        except TimeoutError:
            return False
    
    def play_sound(self, file_path, volume=50):
        '''播放语音
        
        :param file_path: 音频文件路径
        :type  file_path: string
        '''
        from qt4a.androiddriver.util import get_file_md5
        self.set_volume(volume)  # 先设置音量
        file_ext = os.path.splitext(file_path)[-1]
        dst_path = '/data/local/tmp/%s%s' % (get_file_md5(file_path), file_ext)
        self.push_file(file_path, dst_path)
        return self._device_driver.play_sound(dst_path)

    def modify_hosts(self, new_hosts=[]):
        '''修改hosts
        
        :param new_hosts: 要修改的host列表,如果为空,表示恢复为默认hosts
        :type new_hosts: list
        '''
        hosts_path = '/system/etc/hosts'
        bak_hosts_path = hosts_path + '.bak'
        self.adb._set_system_writable()  # 需要system目录可写
        backup = 'echo "127.0.0.1        localhost" > %s' % (hosts_path)
        if new_hosts:
            if not self.is_file_exists(bak_hosts_path):
                # 先生成备份文件
                self.copy_file(hosts_path, bak_hosts_path)
            
            result = self.run_shell_cmd(backup, True)  # 保证当前的hosts文件是干净的
            if 'Permission denied' in result:
                self.run_shell_cmd('chmod 666 %s' % hosts_path, True)
                self.run_shell_cmd(backup, True)

            for ip, host in new_hosts:
                self.run_shell_cmd('echo "%s        %s" >> %s' % (ip, host, hosts_path), True)

            for ip, host in new_hosts:
                real_ip = self.resolve_domain(host)
                if real_ip != ip:
                    raise RuntimeError('设置hosts[%s %s]失败,当前解析值为:%s' % (ip, host, real_ip))
        else:
            # 恢复默认的hosts
            if self.is_file_exists(bak_hosts_path):
                self.copy_file(bak_hosts_path, hosts_path)
            else:
                self.run_shell_cmd(backup, True)

    def set_volume(self, volume):
        '''设置音量
        '''
        return self._device_driver.set_volume(volume)
        
    def get_phone_contacts(self):
        '''获取手机联系人列表
        '''
        return self._device_driver.get_phone_contacts()
        
    def add_phone_contacts(self, name, phone):
        '''添加手机联系人
        '''
        return self._device_driver.add_phone_contacts(name, phone)
    
    def del_phone_contacts(self, name):
        '''删除手机联系人
        '''
        return self._device_driver.del_phone_contacts(name)
    
    def set_app_permission(self, package_name, perm_name, is_allowed=True):
        '''设置应用权限
        
        :param package_name: 应用包名
        :type package_name:  string
        :param perm_name:    权限名称
        :type perm_name:     string
        :param is_allowed:   是否允许
        :type is_allowed:    bool
        '''
        return self._device_driver.set_app_permission(package_name, perm_name, is_allowed)
    
    def set_screen_off_time(self, timeout=600):
        '''设置灭屏时间
        
        :param timeout: 超时时间
        :type timeout:  int,单位为秒
        '''
        if timeout <= 0: timeout = 2147483647
        else: timeout = timeout * 1000
        return self._device_driver.modify_system_setting('system', 'screen_off_timeout', timeout)
    
    def set_auto_rotate_screen(self, rotate=False):
        '''设置是否旋转屏幕
        
        :param rotate: 是否旋转
        :type rotate:  boolean
        '''
        return self._device_driver.modify_system_setting('system', 'accelerometer_rotation', 1 if rotate else 0)
    
    def set_time_12_24(self, is_24=True):
        '''设置12/24小时格式
        
        :param is_24: 是否是24小时
        :type is_24:  boolean
        '''
        return self._device_driver.modify_system_setting('system', 'time_12_24', 24 if is_24 else 12)
    
    def set_allow_unknown_app(self, allow=True):
        '''设置是否允许安装未知来源的应用
        
        :param allow: 是否允许
        :type allow:  boolean
        '''
        return self._device_driver.modify_system_setting('secure', 'install_non_market_apps', 1 if allow else 0)
    
    def set_default_input_method(self, input_method):
        '''设置默认输入法
        
        :param input_method: 要设置的输入法服务名(package_name/service_name)
        :type  input_method: string
        '''
        self._device_driver.modify_system_setting('secure', 'enabled_input_methods', input_method)
        self._device_driver.modify_system_setting('secure', 'default_input_method', input_method)
    
    def connect_vpn(self, vpn_type, server, username, password):
        '''连接VPN
        
        :param vpn_type:  VPN类型
        :type vpn_type:   string
        :param server:    服务器地址
        :type server:     string
        :param username:  用户名
        :type username:   string
        :param password:  密码
        :type password:   string
        '''
        if vpn_type == 'PPTP':
            vpn_type = 0
        elif vpn_type == 'L2TP_IPSEC_PSK':
            vpn_type = 1
        elif vpn_type == 'L2TP_IPSEC_RSA':
            vpn_type = 2
        elif vpn_type == 'IPSEC_XAUTH_PSK':
            vpn_type = 3
        elif vpn_type == 'IPSEC_XAUTH_RSA':
            vpn_type = 4
        elif vpn_type == 'TYPE_IPSEC_HYBRID_RSA':
            vpn_type = 5
        else:
            raise RuntimeError('不支持的VPN类型:%s' % vpn_type)
        
        timeout = 10
        
        for _ in range(3):
            if not self._device_driver.connect_vpn(vpn_type, server, username, password): return False
            time0 = time.time()
            while time.time() - time0 < timeout:
                if self._device_driver.is_vpn_connected(): return True
                time.sleep(1)
        return False
    
    def disconnect_vpn(self):
        '''断开VPN
        '''
        return self._device_driver.disconnect_vpn()
    
    def end_call(self):
        '''挂断电话
        '''
        return self._device_driver.end_call()
    
    def grant_all_runtime_permissions(self, package_name):
        '''给APP授予所有运行时权限
        '''
        return self._device_driver.grant_all_runtime_permissions(package_name)
    
    def set_http_proxy(self, host, port):
        '''设置http代理
        
        :param host: 代理服务器地址
        :param port: 代理服务器端口
        '''
        return self._device_driver.set_http_proxy(host, port)
    
    def clear_http_proxy(self):
        '''清除http代理
        '''
        return self._device_driver.set_http_proxy(None, None)
    
    def register_screenshot_callback(self, callback, frame_rate=15):
        '''注册截图回调函数
        
        :param callback: 回调函数,回调参数为PIL的Image对象
        :type  callback: function
        :param frame_rate: 期望的帧率
        :type  frame_rate: int
        '''
        import copy
        from PIL import Image
        if not hasattr(self, '_screenshot_callbacks'):
            self._screenshot_callbacks = [callback]
            sock = self._device_driver.connect_screenshot_service()
            sock.send(struct.pack('II', 0x3, frame_rate))
            def recv_data(data_len):
                data = ''
                while len(data) < data_len:
                    try:
                        buff = sock.recv(data_len - len(data))
                        if not buff:
                            logger.warn('screenshot socket closed')
                            return
                        data += buff
                    except socket.error as e:
                        logger.warn('recv screenshot data error: %s' % e)
                        return
                return data
            
            def screenshot_thread():
                prev_image = None
                max_width = max_height = 0
                while True:
                    data = recv_data(24)
                    if not data: return
                    timestamp, left, top, width, height, data_len = struct.unpack('I' * 6, data)
                    if data_len > 0:
                        data = recv_data(data_len)
                        assert(len(data) == data_len)
                        fp = BytesIO(data)
                        image = Image.open(fp)
                        # image.verify()
                        w, h = image.size
                        if w > max_width: max_width = w
                        if h > max_height: max_height = h
                        if w < max_width or h < max_height:
                            # 此时prev_image一定不为空
                            try:
                                prev_image.paste(image, (left, top, left + width, top + height))
                            except Exception as e:
                                err_msg = 'compose image [%s]%r failed: %s' % (data_len, (left, top, width, height), e)
                                raise RuntimeError(err_msg)
                        else:
                            prev_image = image
                    for callback in self._screenshot_callbacks:
                        try:
                            callback(copy.deepcopy(prev_image))
                        except:
                            logger.exception('run callback %s failed' % callback.__name__)
            self._screenshot_thread = threading.Thread(target=screenshot_thread)
            self._screenshot_thread.setDaemon(True)
            self._screenshot_thread.start()
        else:
            if not callback in self._screenshot_callbacks: self._screenshot_callbacks.append(callback)
    
    def unregister_screenshot_callback(self, callback):
        '''注销截图回调函数
        
        :param callback: 回调函数
        :type  callback: function
        '''
        if hasattr(self, '_screenshot_callbacks') and callback in self._screenshot_callbacks:
            self._screenshot_callbacks.remove(callback)
 
    def resolve_domain(self, domain):
        '''解析域名
        '''
        return self._device_driver.resolve_domain(domain)

    def set_radio_enabled(self, enable):
        '''是否启用Radio
        '''
        return self._device_driver.set_radio_enabled(enable)

    def get_system_timezone(self):
        '''获取当前系统时区
        '''
        return self.adb.get_property('persist.sys.timezone')

    def set_system_timezone(self, new_timezone='Asia/Shanghai'):
        '''修改系统时区
        '''
        if self.get_system_timezone() != new_timezone:
            self.adb.set_property('persist.sys.timezone', new_timezone)
            
    def set_system_time(self, new_time=None):
        '''设置系统时间

        :param new_time: 新时间,默认为PC上的时间,格式为: 20151001.170000
        :type  new_time: str
        '''
        if not new_time:
            new_time = time.strftime("%Y%m%d.%H%M%S", time.localtime())
        self.adb.run_shell_cmd('date -s %s' % new_time, self.adb.is_rooted())

    def get_available_data_storage(self):
        '''获取数据存储区可用空间
        '''
        return self._device_driver.get_available_storage('/data')

    def get_available_external_storage(self):
        '''获取sdcard可用存储空间
        '''
        return self._device_driver.get_available_storage('/sdcard')
Пример #3
0
def copy_android_driver(device_id_or_adb,
                        force=False,
                        root_path=None,
                        enable_acc=True):
    """测试前的测试桩拷贝
    """
    from qt4a.androiddriver.adb import ADB

    if isinstance(device_id_or_adb, ADB):
        adb = device_id_or_adb
    else:
        adb = ADB.open_device(device_id_or_adb)

    if not root_path:
        current_path = os.path.abspath(__file__)
        if not os.path.exists(current_path) and ".egg" in current_path:
            # in egg
            egg_path = current_path
            while not os.path.exists(egg_path):
                egg_path = os.path.dirname(egg_path)
            assert egg_path.endswith(".egg")
            root_path = os.path.join(tempfile.mkdtemp(), "tools")
            extract_from_zipfile(egg_path, "qt4a/androiddriver/tools",
                                 root_path)
        else:
            root_path = os.path.join(
                os.path.dirname(os.path.abspath(__file__)), "tools")
    dst_path = "/data/local/tmp/qt4a/"

    current_version_file = os.path.join(root_path, "version.txt")
    f = open(current_version_file, "r")
    current_version = int(f.read())
    f.close()

    if not force:
        version_file = dst_path + "version.txt"
        version = adb.run_shell_cmd("cat %s" % version_file)

        if (version and not "No such file or directory" in version
                and current_version <= int(version)):
            install_qt4a_helper(adb, root_path)  # 避免QT4A助手被意外删除的情况
            # 不需要拷贝测试桩
            logger.warn("忽略本次测试桩拷贝:当前版本为%s,设备中版本为%s" %
                        (current_version, int(version)))
            return

    try:
        adb.chmod(dst_path[:-1], "777")
    except:
        pass

    rooted = adb.is_rooted()

    cpu_abi = adb.get_cpu_abi()
    print("Current CPU arch: %s" % cpu_abi)
    # use_pie = False
    # if adb.get_sdk_version() >= 21 and cpu_abi != "arm64-v8a":
    #     use_pie = True

    file_list = [
        os.path.join(cpu_abi, "droid_inject"),
        os.path.join(cpu_abi, "libdexloader.so"),
        os.path.join(cpu_abi, "screenkit"),
        "inject",
        "AndroidSpy.jar",
        "SpyHelper.jar",
        "SpyHelper.sh",
    ]
    if cpu_abi == "arm64-v8a":
        file_list.append(os.path.join(cpu_abi, "droid_inject64"))
        file_list.append(os.path.join(cpu_abi, "libdexloader64.so"))
        file_list.append("inject64")
    if adb.get_sdk_version() >= 21:
        file_list.append(os.path.join(cpu_abi, "libandroidhook_art.so"))

    if rooted and adb.is_selinux_opened():
        # 此时如果还是开启状态说明关闭selinux没有生效,主要是三星手机上面
        adb.run_shell_cmd("rm -r %s" % dst_path, True)
        # adb.run_shell_cmd('chcon u:object_r:shell_data_file:s0 %slibdexloader.so' % dst_path, True)  # 恢复文件context,否则拷贝失败
        # adb.run_shell_cmd('chcon u:object_r:shell_data_file:s0 %slibandroidhook.so' % dst_path, True)

    for file in file_list:
        file_path = os.path.join(root_path, file)
        # if use_pie and not "." in file and os.path.exists(file_path + "_pie"):
        #     file_path += "_pie"
        if not os.path.exists(file_path):
            continue
        save_name = os.path.split(file)[-1]
        if save_name.endswith("_art.so"):
            save_name = save_name.replace("_art", "")
        adb.push_file(file_path, dst_path + save_name)

    adb.chmod("%sdroid_inject" % dst_path, 755)
    adb.chmod("%sinject" % dst_path, 755)
    adb.chmod("%sscreenkit" % dst_path, 755)
    adb.run_shell_cmd("ln -s %sscreenkit %sscreenshot" % (dst_path, dst_path))

    if cpu_abi == "arm64-v8a":
        adb.chmod("%sdroid_inject64" % dst_path, 755)
        adb.chmod("%sinject64" % dst_path, 755)

    try:
        print(adb.run_shell_cmd("rm -R %scache" % dst_path,
                                rooted))  # 删除目录 rm -rf
    except RuntimeError as e:
        logger.warn("%s" % e)
    # logger.info(adb.run_shell_cmd('mkdir %scache' % (dst_path), True)) #必须使用root权限,不然生成odex文件会失败

    adb.mkdir("%scache" % (dst_path), 777)

    install_qt4a_helper(adb, root_path)

    version_file_path = os.path.join(root_path, "version.txt")
    dst_version_file_path = dst_path + os.path.split(version_file_path)[-1]
    adb.push_file(version_file_path, dst_version_file_path + ".tmp")  # 拷贝版本文件

    if rooted and adb.is_selinux_opened():
        # 此时如果还是开启状态说明关闭selinux没有生效,主要是三星手机上面
        # 获取sdcars context
        if adb.get_sdk_version() >= 23:
            sdcard_path = adb.get_sdcard_path()
            result = adb.run_shell_cmd("ls -Z %s" % sdcard_path)
            # u:object_r:media_rw_data_file:s0 u:object_r:rootfs:s0
            pattern = re.compile(r"\s+(u:object_r:.+:s0)\s+")
            ret = pattern.search(result)
            if not ret:
                raise RuntimeError("get sdcard context failed: %s" % result)
            context = ret.group(1)
            logger.info("sdcard context is %s" % context)
            adb.run_shell_cmd("chcon %s %s" % (context, dst_path),
                              True)  # make app access
            adb.run_shell_cmd(
                "chcon u:object_r:app_data_file:s0 %sSpyHelper.jar" % dst_path,
                True)
            adb.run_shell_cmd(
                "chcon u:object_r:app_data_file:s0 %sSpyHelper.sh" % dst_path,
                True)
            # 不修改文件context无法加载so
            adb.run_shell_cmd(
                "chcon u:object_r:system_file:s0 %slibdexloader.so" % dst_path,
                True)
            adb.run_shell_cmd(
                "chcon u:object_r:app_data_file:s0 %slibandroidhook.so" %
                dst_path, True)
            adb.run_shell_cmd(
                "chcon %s %sAndroidSpy.jar" % (context, dst_path), True)
            adb.run_shell_cmd("chcon %s %scache" % (context, dst_path), True)
        else:
            # 不修改文件context无法加载so
            adb.run_shell_cmd(
                "chcon u:object_r:app_data_file:s0 %slibdexloader.so" %
                dst_path, True)
            adb.run_shell_cmd(
                "chcon u:object_r:app_data_file:s0 %slibandroidhook.so" %
                dst_path, True)
            adb.run_shell_cmd(
                "chcon u:object_r:app_data_file:s0 %scache" % dst_path, True)

    if rooted:
        if adb.get_sdk_version() < 24:
            # 7.0以上发现生成的dex与运行时生成的dex有差别,可能导致crash
            logger.info(
                adb.run_shell_cmd(
                    "sh %sSpyHelper.sh loadDex %sAndroidSpy.jar %scache" %
                    (dst_path, dst_path, dst_path),
                    rooted,
                ))
            adb.chmod("%scache/AndroidSpy.dex" % dst_path, 666)
    else:
        if not "usage:" in adb.run_shell_cmd("sh %sSpyHelper.sh" % dst_path):
            adb.mkdir("%scache/dalvik-cache" % dst_path, 777)

    if rooted and adb.is_selinux_opened() and adb.get_sdk_version() >= 23:
        # 提升权限
        try:
            adb.list_dir("/system/bin/app_process32")
        except RuntimeError:
            adb.copy_file("/system/bin/app_process",
                          "%sapp_process" % dst_path)
        else:
            adb.copy_file("/system/bin/app_process32",
                          "%sapp_process" % dst_path)
        adb.chmod("%sapp_process" % dst_path, 755)
        adb.run_shell_cmd(
            "chcon u:object_r:system_file:s0 %sapp_process" % dst_path, True)

    adb.run_shell_cmd(
        "mv %s %s" % (dst_version_file_path + ".tmp", dst_version_file_path),
        rooted)

    # 同步手机时间
    device_driver = DeviceDriver(adb)
    try:
        input_method = "com.test.androidspy/.service.QT4AKeyboardService"
        device_driver.modify_system_setting("secure", "enabled_input_methods",
                                            input_method)
        device_driver.modify_system_setting("secure", "default_input_method",
                                            input_method)
        if enable_acc:
            device_driver.modify_system_setting(
                "secure",
                "enabled_accessibility_services",
                "com.test.androidspy/com.test.androidspy.service.QT4AAccessibilityService",
            )
            device_driver.modify_system_setting("secure",
                                                "accessibility_enabled", 1)
    except:
        logger.exception("set default input method failed")
    try:
        device_driver.modify_system_setting("system", "time_12_24", 24)
        device_driver.modify_system_setting("system", "screen_off_timeout",
                                            600 * 1000)
    except:
        logger.exception("set system time failed")
Пример #4
0
def copy_android_driver(device_id_or_adb,
                        force=False,
                        root_path=None,
                        enable_acc=True):
    '''测试前的测试桩拷贝
    '''
    from qt4a.androiddriver.adb import ADB
    from qt4a.androiddriver.util import AndroidPackage, version_cmp

    if isinstance(device_id_or_adb, ADB):
        adb = device_id_or_adb
    else:
        adb = ADB.open_device(device_id_or_adb)

    if not root_path:
        current_path = os.path.abspath(__file__)
        if not os.path.exists(current_path) and '.egg' in current_path:
            # in egg
            egg_path = current_path
            while not os.path.exists(egg_path):
                egg_path = os.path.dirname(egg_path)
            assert (egg_path.endswith('.egg'))
            root_path = os.path.join(tempfile.mkdtemp(), 'tools')
            extract_from_zipfile(egg_path, 'qt4a/androiddriver/tools',
                                 root_path)
        else:
            root_path = os.path.join(
                os.path.dirname(os.path.abspath(__file__)), 'tools')
    dst_path = '/data/local/tmp/qt4a/'

    current_version_file = os.path.join(root_path, 'version.txt')
    f = open(current_version_file, 'r')
    current_version = int(f.read())
    f.close()

    if not force:
        version_file = dst_path + 'version.txt'
        version = adb.run_shell_cmd('cat %s' % version_file)

        if version and not 'No such file or directory' in version and current_version <= int(
                version):
            # 不需要拷贝测试桩
            logger.warn('忽略本次测试桩拷贝:当前版本为%s,设备中版本为%s' %
                        (current_version, int(version)))
            return

    try:
        adb.chmod(dst_path[:-1], '777')
    except:
        pass

    rooted = adb.is_rooted()

    cpu_abi = adb.get_cpu_abi()
    print('Current CPU arch: %s' % cpu_abi)
    use_pie = False
    if adb.get_sdk_version() >= 21 and cpu_abi != 'arm64-v8a':
        use_pie = True

    file_list = [
        os.path.join(cpu_abi, 'droid_inject'),
        os.path.join(cpu_abi, 'libdexloader.so'),
        os.path.join(cpu_abi, 'screenkit'),
        os.path.join(cpu_abi, 'libandroidhook.so'), 'inject', 'AndroidSpy.jar',
        'SpyHelper.jar', 'SpyHelper.sh'
    ]

    if cpu_abi == 'arm64-v8a':
        file_list.append(os.path.join(cpu_abi, 'droid_inject64'))
        file_list.append(os.path.join(cpu_abi, 'libdexloader64.so'))
        file_list.append('inject64')
    if adb.get_sdk_version() >= 21:
        file_list.append(os.path.join(cpu_abi, 'libandroidhook_art.so'))

    if rooted and adb.is_selinux_opened():
        # 此时如果还是开启状态说明关闭selinux没有生效,主要是三星手机上面
        adb.run_shell_cmd('rm -r %s' % dst_path, True)
        # adb.run_shell_cmd('chcon u:object_r:shell_data_file:s0 %slibdexloader.so' % dst_path, True)  # 恢复文件context,否则拷贝失败
        # adb.run_shell_cmd('chcon u:object_r:shell_data_file:s0 %slibandroidhook.so' % dst_path, True)

    for file in file_list:
        file_path = os.path.join(root_path, file)
        if use_pie and not '.' in file and os.path.exists(file_path + '_pie'):
            file_path += '_pie'
        if not os.path.exists(file_path):
            continue
        save_name = os.path.split(file)[-1]
        if save_name.endswith('_art.so'):
            save_name = save_name.replace('_art', '')
        adb.push_file(file_path, dst_path + save_name)

    adb.chmod('%sdroid_inject' % dst_path, 755)
    adb.chmod('%sinject' % dst_path, 755)
    adb.chmod('%sscreenkit' % dst_path, 755)
    adb.run_shell_cmd('ln -s %sscreenkit %sscreenshot' % (dst_path, dst_path))

    if cpu_abi == 'arm64-v8a':
        adb.chmod('%sdroid_inject64' % dst_path, 755)
        adb.chmod('%sinject64' % dst_path, 755)

    try:
        print(adb.run_shell_cmd('rm -R %scache' % dst_path,
                                rooted))  # 删除目录 rm -rf
    except RuntimeError as e:
        logger.warn('%s' % e)
    # logger.info(adb.run_shell_cmd('mkdir %scache' % (dst_path), True)) #必须使用root权限,不然生成odex文件会失败

    adb.mkdir('%scache' % (dst_path), 777)

    qt4a_helper_package = 'com.test.androidspy'
    apk_path = os.path.join(root_path, 'QT4AHelper.apk')
    if adb.get_package_path(qt4a_helper_package):
        # 判断版本
        installed_version = adb.get_package_version(qt4a_helper_package)
        package = AndroidPackage(apk_path)
        install_version = package.version
        if version_cmp(install_version, installed_version) > 0:
            adb.install_apk(apk_path, True)
    else:
        adb.install_apk(apk_path)

    # adb.install_apk(os.path.join(root_path, 'QT4AMockApp.apk'), True)
    version_file_path = os.path.join(root_path, 'version.txt')
    dst_version_file_path = dst_path + os.path.split(version_file_path)[-1]
    adb.push_file(version_file_path, dst_version_file_path + '.tmp')  # 拷贝版本文件

    if rooted and adb.is_selinux_opened():
        # 此时如果还是开启状态说明关闭selinux没有生效,主要是三星手机上面
        # 获取sdcars context
        if adb.get_sdk_version() >= 23:
            import re
            sdcard_path = adb.get_sdcard_path()
            result = adb.run_shell_cmd('ls -Z %s' % sdcard_path)
            # u:object_r:media_rw_data_file:s0 u:object_r:rootfs:s0
            pattern = re.compile(r'\s+(u:object_r:.+:s0)\s+')
            ret = pattern.search(result)
            if not ret:
                raise RuntimeError('get sdcard context failed: %s' % result)
            context = ret.group(1)
            logger.info('sdcard context is %s' % context)
            adb.run_shell_cmd('chcon %s %s' % (context, dst_path),
                              True)  # make app access
            adb.run_shell_cmd(
                'chcon u:object_r:app_data_file:s0 %sSpyHelper.jar' % dst_path,
                True)
            adb.run_shell_cmd(
                'chcon u:object_r:app_data_file:s0 %sSpyHelper.sh' % dst_path,
                True)
            # 不修改文件context无法加载so
            adb.run_shell_cmd(
                'chcon u:object_r:system_file:s0 %slibdexloader.so' % dst_path,
                True)
            adb.run_shell_cmd(
                'chcon u:object_r:app_data_file:s0 %slibandroidhook.so' %
                dst_path, True)
            adb.run_shell_cmd(
                'chcon %s %sAndroidSpy.jar' % (context, dst_path), True)
            adb.run_shell_cmd('chcon %s %scache' % (context, dst_path), True)
        else:
            # 不修改文件context无法加载so
            adb.run_shell_cmd(
                'chcon u:object_r:app_data_file:s0 %slibdexloader.so' %
                dst_path, True)
            adb.run_shell_cmd(
                'chcon u:object_r:app_data_file:s0 %slibandroidhook.so' %
                dst_path, True)
            adb.run_shell_cmd(
                'chcon u:object_r:app_data_file:s0 %scache' % dst_path, True)

    if rooted:
        if adb.get_sdk_version() < 24:
            # 7.0以上发现生成的dex与运行时生成的dex有差别,可能导致crash
            logger.info(
                adb.run_shell_cmd(
                    'sh %sSpyHelper.sh loadDex %sAndroidSpy.jar %scache' %
                    (dst_path, dst_path, dst_path), rooted))
            adb.chmod('%scache/AndroidSpy.dex' % dst_path, 666)
    else:
        if not 'usage:' in adb.run_shell_cmd('sh %sSpyHelper.sh' % dst_path):
            adb.mkdir('%scache/dalvik-cache' % dst_path, 777)

    if rooted and adb.is_selinux_opened() and adb.get_sdk_version() >= 23:
        # 提升权限
        try:
            adb.list_dir('/system/bin/app_process32')
        except RuntimeError:
            adb.copy_file('/system/bin/app_process',
                          '%sapp_process' % dst_path)
        else:
            adb.copy_file('/system/bin/app_process32',
                          '%sapp_process' % dst_path)
        adb.chmod('%sapp_process' % dst_path, 755)
        adb.run_shell_cmd(
            'chcon u:object_r:system_file:s0 %sapp_process' % dst_path, True)

    adb.run_shell_cmd(
        'mv %s %s' % (dst_version_file_path + '.tmp', dst_version_file_path),
        rooted)

    # 同步手机时间
    device_driver = DeviceDriver(adb)
    try:
        input_method = 'com.test.androidspy/.service.QT4AKeyboardService'
        device_driver.modify_system_setting('secure', 'enabled_input_methods',
                                            input_method)
        device_driver.modify_system_setting('secure', 'default_input_method',
                                            input_method)
        if enable_acc:
            device_driver.modify_system_setting(
                'secure', 'enabled_accessibility_services',
                'com.test.androidspy/com.test.androidspy.service.QT4AAccessibilityService'
            )
            device_driver.modify_system_setting('secure',
                                                'accessibility_enabled', 1)
    except:
        logger.exception('set default input method failed')
    try:
        device_driver.modify_system_setting('system', 'time_12_24', 24)
        device_driver.modify_system_setting('system', 'screen_off_timeout',
                                            600 * 1000)
    except:
        logger.exception('set system time failed')
Пример #5
0
 def _create_driver(self):
     adb_backend = LocalADBBackend('127.0.0.1', '')
     adb = ADB(adb_backend)
     return AndroidDriver(DeviceDriver(adb), None)
Пример #6
0
 def _get_device_driver(self):
     ADB.run_shell_cmd = mock.Mock(side_effect=mock_run_shell_cmd)
     adb_backend = LocalADBBackend('127.0.0.1', '')
     adb = ADB(adb_backend)
     return DeviceDriver(adb)