You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
324 lines
10 KiB
324 lines
10 KiB
import datetime
|
|
import json
|
|
import os
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import time
|
|
import shlex
|
|
from android_db import AndroidSQLConn
|
|
from load_things import loaded
|
|
from decode_parcel import decode_parcel
|
|
debug = True
|
|
config = loaded.config
|
|
keycodes = loaded.keycodes
|
|
exe = config.defaults.exe
|
|
|
|
|
|
def merge(src, dst, log=False):
|
|
if not os.path.exists(dst):
|
|
return False
|
|
ok = True
|
|
for path, dirs, files in os.walk(src):
|
|
relPath = os.path.relpath(path, src)
|
|
destPath = os.path.join(dst, relPath)
|
|
if not os.path.exists(destPath):
|
|
os.makedirs(destPath)
|
|
for file in files:
|
|
destFile = os.path.join(destPath, file)
|
|
if os.path.isfile(destFile):
|
|
if log:
|
|
print("Skipping existing file: " +
|
|
os.path.join(relPath, file))
|
|
ok = False
|
|
continue
|
|
srcFile = os.path.join(path, file)
|
|
shutil.move(srcFile, destFile)
|
|
for path, dirs, files in os.walk(src, False):
|
|
if len(files) == 0 and len(dirs) == 0:
|
|
os.rmdir(path)
|
|
return ok
|
|
|
|
|
|
def _adb(*args, output="shell"):
|
|
'''Output modes:
|
|
"out": return output
|
|
"shell": print to shell
|
|
"buffered": read line by line'''
|
|
args = [exe] + list(args)
|
|
if output == "out":
|
|
return subprocess.check_output(args, shell=False).decode().replace('\r\n', '\n').rstrip()
|
|
elif output == "shell":
|
|
ret = subprocess.call(args, shell=False)
|
|
if ret:
|
|
raise subprocess.CalledProcessError(ret, args)
|
|
elif output == "buffered":
|
|
p = subprocess.Popen(args, stdout=subprocess.PIPE)
|
|
return p.stdout
|
|
|
|
|
|
def kill_server():
|
|
_adb('kill-server')
|
|
|
|
|
|
def start_server():
|
|
_adb('start-server')
|
|
|
|
|
|
def get_info():
|
|
start_server()
|
|
thing = _adb("devices", "-l", output="out")
|
|
formed = list(filter(bool, thing.split("\n")))[1:]
|
|
main = {}
|
|
for device in formed:
|
|
categories = re.split(" +", device)
|
|
device_dict = {
|
|
"serial": categories[0],
|
|
"mode": categories[1]
|
|
}
|
|
device_dict.update(dict(category.split(":")
|
|
for category in categories[2:]))
|
|
main[categories[0]] = device_dict
|
|
return main
|
|
|
|
|
|
class ADBWrapper:
|
|
root_mode = False
|
|
|
|
def connect(ip, port=5555):
|
|
if not re.match(r'(\d{1,3}\.){3}\d{1,3}', ip):
|
|
raise TypeError("Invalid ip")
|
|
if not all(int(n) <= 255 and int(n) >= 0 for n in ip.split('.')):
|
|
raise TypeError("Invalid ip")
|
|
if not (port >= 0 and port <= 2**16-1):
|
|
raise TyperError("Port must be in the range 0-65536")
|
|
id = '{}:{}'.format(ip, port)
|
|
_adb('connect', '{}:{}'.format(ip, port))
|
|
dev = Device(id)
|
|
dev.tcip = True
|
|
return dev
|
|
|
|
def disconnect(self):
|
|
if self.tcip:
|
|
_adb('disconnect', self.serial)
|
|
|
|
def db_connect(self, filepath):
|
|
return AndroidSQLConn(self, filepath)
|
|
|
|
def prim_device():
|
|
cont = True
|
|
while cont:
|
|
try:
|
|
d = Device()
|
|
cont = False
|
|
except IndexError:
|
|
time.sleep(1)
|
|
return d
|
|
|
|
def __init__(self, serial=None):
|
|
self.tcip = False
|
|
if serial:
|
|
self.serial = serial
|
|
info = get_info()[serial]
|
|
else:
|
|
serial, info = list(get_info().items())[0]
|
|
self.__dict__.update(info)
|
|
|
|
def adb(self, *args, output="shell"):
|
|
args = ['-s', self.serial] + list(args)
|
|
return _adb(*args, output=output)
|
|
|
|
def shell(self, *args, output="shell"):
|
|
args = ('shell',)+args
|
|
return self.adb(*args, output=output)
|
|
|
|
def sudo(self, *args, output="shell"):
|
|
if self.mode == 'recovery' or self.root_mode:
|
|
return self.shell(*args, output=output)
|
|
else:
|
|
return self.shell('su', '--', '--', *args, output=output)
|
|
|
|
@classmethod
|
|
def root(cls):
|
|
cls.root_mode = True
|
|
_adb('root')
|
|
|
|
@classmethod
|
|
def unroot(cls):
|
|
cls.root_mode = False
|
|
_adb('unroot')
|
|
|
|
def reboot(self, mode=None):
|
|
if mode:
|
|
if mode == "soft":
|
|
if self.mode != 'recovery':
|
|
pid = self.shell("pidof", "zygote", output="out")
|
|
return self.sudo("kill", pid, output="shell")
|
|
else:
|
|
return self.reboot()
|
|
else:
|
|
self.adb("reboot", mode)
|
|
else:
|
|
self.adb("reboot")
|
|
while True:
|
|
infos = get_info()
|
|
if len(infos) > 0:
|
|
self.__dict__.update(infos[self.serial])
|
|
break
|
|
time.sleep(1)
|
|
|
|
|
|
class FSActionWrapper(ADBWrapper):
|
|
|
|
def stat(self, file):
|
|
'''\
|
|
%a Access bits (octal) |%A Access bits (flags)|%b Size/512
|
|
%B Bytes per %b (512) |%d Device ID (dec) |%D Device ID (hex)
|
|
%f All mode bits (hex) |%F File type |%g Group ID
|
|
%G Group name |%h Hard links |%i Inode
|
|
%m Mount point |%n Filename |%N Long filename
|
|
%o I/O block size |%s Size (bytes) |%t Devtype major (hex)
|
|
%T Devtype minor (hex) |%u User ID |%U User name
|
|
%x Access time |%X Access unix time |%y File write time
|
|
%Y File write unix time|%z Dir change time |%Z Dir change unix time
|
|
|
|
The valid format escape sequences for filesystems:
|
|
%a Available blocks |%b Total blocks |%c Total inodes
|
|
%d Free inodes |%f Free blocks |%i File system ID
|
|
%l Max filename length |%n File name |%s Fragment size
|
|
%S Best transfer size |%t FS type (hex) |%T FS type (driver name)'''
|
|
command = 'stat -c "%A;%F;%U;%G" {};echo $?'.format(file)
|
|
res = self.sudo(command, output="out")
|
|
output, res = res.split('\n')
|
|
if res == '0':
|
|
return output.split(';')
|
|
|
|
def exists(self, file):
|
|
return self.stat(file) is not None
|
|
|
|
def isfile(self, file):
|
|
return self.stat(file)[1] == 'file'
|
|
|
|
def isdir(self, file):
|
|
return self.stat(file)[1] == 'directory'
|
|
|
|
def islink(self, file):
|
|
return self.stat(file)[1] == 'symbolic link'
|
|
|
|
def delete(self, path):
|
|
return self.sudo("rm", "-rf", path, output="out")
|
|
|
|
def copy(self, remote, local, del_duplicates=True, ignore_error=True):
|
|
remote_stat = self.stat(remote)
|
|
if remote_stat is not None:
|
|
if remote_stat[1] == "directory" and not remote.endswith('/'):
|
|
remote += '/'
|
|
merge_flag = False
|
|
if os.path.exists(local):
|
|
last = os.path.split(local)[-1]
|
|
real_dir = local
|
|
local = os.path.join(config['local']['temp'], last)
|
|
merge_flag = True
|
|
try:
|
|
self.adb("pull", "-a", remote, local)
|
|
except subprocess.CalledProcessError as e:
|
|
if ignore_error:
|
|
pass
|
|
else:
|
|
raise e
|
|
if merge_flag:
|
|
merge(local, real_dir)
|
|
if os.path.exists(local) and del_duplicates:
|
|
shutil.rmtree(local)
|
|
else:
|
|
print("File not found: {}".format(remote))
|
|
|
|
def move(self, remote, local, del_duplicates=True, ignore_error=False):
|
|
if self.exists(remote):
|
|
self.copy(remote, local, del_duplicates=del_duplicates,
|
|
ignore_error=ignore_error)
|
|
self.delete(remote)
|
|
else:
|
|
print("File not found: {}".format(remote))
|
|
|
|
def push(self, local, remote):
|
|
self.adb('push', local, remote)
|
|
|
|
|
|
class Input(ADBWrapper):
|
|
|
|
def send_keycode(self, code):
|
|
try:
|
|
keycode = keycodes[code]
|
|
except KeyError:
|
|
keycode = str(code)
|
|
self.shell("input", "keyevent", keycode)
|
|
|
|
def unlock_phone(self, password):
|
|
if self.mode == 'recovery':
|
|
return
|
|
if not decode_parcel(self.shell('service', 'call', 'power', '12', output="out"), 'int'):
|
|
self.send_keycode('power')
|
|
if decode_parcel(self.shell('service', 'call', 'trust', '7', output="out"), 'int'):
|
|
self.send_keycode('space')
|
|
self.shell("input", "text", str(password))
|
|
self.send_keycode('enter')
|
|
|
|
|
|
class TWRP(FSActionWrapper):
|
|
|
|
def backup(self, *partitions, name=None, backupdir=None):
|
|
if self.mode != 'recovery':
|
|
self.reboot('recovery')
|
|
if backupdir is None:
|
|
backupdir = config['local']['twrp']
|
|
else:
|
|
backupdir = backupdir
|
|
options_dict = {
|
|
"system": "S",
|
|
"data": "D",
|
|
"cache": "C",
|
|
"recovery": "R",
|
|
"spec_part_1": "1",
|
|
"spec_part_2": "2",
|
|
"spec_part_3": "3",
|
|
"boot": "B",
|
|
"as": "A"
|
|
}
|
|
options = "".join(options_dict[option] for option in partitions)
|
|
if not name:
|
|
name = "backup_" + \
|
|
datetime.datetime.today().strftime(defaults['date_format'])
|
|
filename = os.path.join(backupdir, name)
|
|
self.shell("twrp", "backup", options, name)
|
|
phone_dir = "/data/media/0/TWRP/BACKUPS/{serial}/{name}".format(
|
|
serial=self.serial, name=name)
|
|
self.move(phone_dir, filename)
|
|
|
|
def wipe(self, partition):
|
|
if self.mode != 'recovery':
|
|
self.reboot('recovery')
|
|
self.shell("twrp", "wipe", partition)
|
|
|
|
def install(self, name):
|
|
if self.mode != 'recovery':
|
|
self.reboot('recovery')
|
|
if os.path.exists(name):
|
|
local_name = name
|
|
name = os.path.split(name)[-1]
|
|
update_path = '{}/{}'.format(config['remote']['updates'], name)
|
|
if not self.exists(config['remote']['updates']):
|
|
self.sudo('mkdir', config['remote']['updates'])
|
|
if not self.exists(update_path):
|
|
self.push(local_name, config['remote']['updates'])
|
|
else:
|
|
update_path = '{}/{}'.format(config['remote']['updates'], name)
|
|
self.shell("twrp", "install", update_path)
|
|
|
|
|
|
class Device(TWRP, Input):
|
|
pass
|
|
|
|
|
|
if __name__ == "__main__" and debug:
|
|
d = Device.prim_device()
|