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

8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
  1. import datetime
  2. import json
  3. import os
  4. import re
  5. import shutil
  6. import subprocess
  7. import time
  8. import shlex
  9. from android_db import AndroidSQLConn
  10. from load_things import loaded
  11. from decode_parcel import decode_parcel
  12. debug = True
  13. config = loaded.config
  14. keycodes = loaded.keycodes
  15. exe = config.defaults.exe
  16. def merge(src, dst, log=False):
  17. if not os.path.exists(dst):
  18. return False
  19. ok = True
  20. for path, dirs, files in os.walk(src):
  21. relPath = os.path.relpath(path, src)
  22. destPath = os.path.join(dst, relPath)
  23. if not os.path.exists(destPath):
  24. os.makedirs(destPath)
  25. for file in files:
  26. destFile = os.path.join(destPath, file)
  27. if os.path.isfile(destFile):
  28. if log:
  29. print("Skipping existing file: " +
  30. os.path.join(relPath, file))
  31. ok = False
  32. continue
  33. srcFile = os.path.join(path, file)
  34. shutil.move(srcFile, destFile)
  35. for path, dirs, files in os.walk(src, False):
  36. if len(files) == 0 and len(dirs) == 0:
  37. os.rmdir(path)
  38. return ok
  39. def _adb(*args, output="shell"):
  40. '''Output modes:
  41. "out": return output
  42. "shell": print to shell
  43. "buffered": read line by line'''
  44. args = [exe] + list(args)
  45. if output == "out":
  46. return subprocess.check_output(args, shell=False).decode().replace('\r\n', '\n').rstrip()
  47. elif output == "shell":
  48. ret = subprocess.call(args, shell=False)
  49. if ret:
  50. raise subprocess.CalledProcessError(ret, args)
  51. elif output == "buffered":
  52. p = subprocess.Popen(args, stdout=subprocess.PIPE)
  53. return p.stdout
  54. def kill_server():
  55. _adb('kill-server')
  56. def start_server():
  57. _adb('start-server')
  58. def get_info():
  59. start_server()
  60. thing = _adb("devices", "-l", output="out")
  61. formed = list(filter(bool, thing.split("\n")))[1:]
  62. main = {}
  63. for device in formed:
  64. categories = re.split(" +", device)
  65. device_dict = {
  66. "serial": categories[0],
  67. "mode": categories[1]
  68. }
  69. device_dict.update(dict(category.split(":")
  70. for category in categories[2:]))
  71. main[categories[0]] = device_dict
  72. return main
  73. class ADBWrapper:
  74. root_mode = False
  75. def connect(ip, port=5555):
  76. if not re.match(r'(\d{1,3}\.){3}\d{1,3}', ip):
  77. raise TypeError("Invalid ip")
  78. if not all(int(n) <= 255 and int(n) >= 0 for n in ip.split('.')):
  79. raise TypeError("Invalid ip")
  80. if not (port >= 0 and port <= 2**16-1):
  81. raise TyperError("Port must be in the range 0-65536")
  82. id = '{}:{}'.format(ip, port)
  83. _adb('connect', '{}:{}'.format(ip, port))
  84. dev = Device(id)
  85. dev.tcip = True
  86. return dev
  87. def disconnect(self):
  88. if self.tcip:
  89. _adb('disconnect', self.serial)
  90. def db_connect(self, filepath):
  91. return AndroidSQLConn(self, filepath)
  92. def prim_device():
  93. cont = True
  94. while cont:
  95. try:
  96. d = Device()
  97. cont = False
  98. except IndexError:
  99. time.sleep(1)
  100. return d
  101. def __init__(self, serial=None):
  102. self.tcip = False
  103. if serial:
  104. self.serial = serial
  105. info = get_info()[serial]
  106. else:
  107. serial, info = list(get_info().items())[0]
  108. self.__dict__.update(info)
  109. def adb(self, *args, output="shell"):
  110. args = ['-s', self.serial] + list(args)
  111. return _adb(*args, output=output)
  112. def shell(self, *args, output="shell"):
  113. args = ('shell',)+args
  114. return self.adb(*args, output=output)
  115. def sudo(self, *args, output="shell"):
  116. if self.mode == 'recovery' or self.root_mode:
  117. return self.shell(*args, output=output)
  118. else:
  119. return self.shell('su', '--', '--', *args, output=output)
  120. @classmethod
  121. def root(cls):
  122. cls.root_mode = True
  123. _adb('root')
  124. @classmethod
  125. def unroot(cls):
  126. cls.root_mode = False
  127. _adb('unroot')
  128. def reboot(self, mode=None):
  129. if mode:
  130. if mode == "soft":
  131. if self.mode != 'recovery':
  132. pid = self.shell("pidof", "zygote", output="out")
  133. return self.sudo("kill", pid, output="shell")
  134. else:
  135. return self.reboot()
  136. else:
  137. self.adb("reboot", mode)
  138. else:
  139. self.adb("reboot")
  140. while True:
  141. infos = get_info()
  142. if len(infos) > 0:
  143. self.__dict__.update(infos[self.serial])
  144. break
  145. time.sleep(1)
  146. class FSActionWrapper(ADBWrapper):
  147. def stat(self, file):
  148. '''\
  149. %a Access bits (octal) |%A Access bits (flags)|%b Size/512
  150. %B Bytes per %b (512) |%d Device ID (dec) |%D Device ID (hex)
  151. %f All mode bits (hex) |%F File type |%g Group ID
  152. %G Group name |%h Hard links |%i Inode
  153. %m Mount point |%n Filename |%N Long filename
  154. %o I/O block size |%s Size (bytes) |%t Devtype major (hex)
  155. %T Devtype minor (hex) |%u User ID |%U User name
  156. %x Access time |%X Access unix time |%y File write time
  157. %Y File write unix time|%z Dir change time |%Z Dir change unix time
  158. The valid format escape sequences for filesystems:
  159. %a Available blocks |%b Total blocks |%c Total inodes
  160. %d Free inodes |%f Free blocks |%i File system ID
  161. %l Max filename length |%n File name |%s Fragment size
  162. %S Best transfer size |%t FS type (hex) |%T FS type (driver name)'''
  163. command = 'stat -c "%A;%F;%U;%G" {};echo $?'.format(file)
  164. res = self.sudo(command, output="out")
  165. output, res = res.split('\n')
  166. if res == '0':
  167. return output.split(';')
  168. def exists(self, file):
  169. return self.stat(file) is not None
  170. def isfile(self, file):
  171. return self.stat(file)[1] == 'file'
  172. def isdir(self, file):
  173. return self.stat(file)[1] == 'directory'
  174. def islink(self, file):
  175. return self.stat(file)[1] == 'symbolic link'
  176. def delete(self, path):
  177. return self.sudo("rm", "-rf", path, output="out")
  178. def copy(self, remote, local, del_duplicates=True, ignore_error=True):
  179. remote_stat = self.stat(remote)
  180. if remote_stat is not None:
  181. if remote_stat[1] == "directory" and not remote.endswith('/'):
  182. remote += '/'
  183. merge_flag = False
  184. if os.path.exists(local):
  185. last = os.path.split(local)[-1]
  186. real_dir = local
  187. local = os.path.join(config['local']['temp'], last)
  188. merge_flag = True
  189. try:
  190. self.adb("pull", "-a", remote, local)
  191. except subprocess.CalledProcessError as e:
  192. if ignore_error:
  193. pass
  194. else:
  195. raise e
  196. if merge_flag:
  197. merge(local, real_dir)
  198. if os.path.exists(local) and del_duplicates:
  199. shutil.rmtree(local)
  200. else:
  201. print("File not found: {}".format(remote))
  202. def move(self, remote, local, del_duplicates=True, ignore_error=False):
  203. if self.exists(remote):
  204. self.copy(remote, local, del_duplicates=del_duplicates,
  205. ignore_error=ignore_error)
  206. self.delete(remote)
  207. else:
  208. print("File not found: {}".format(remote))
  209. def push(self, local, remote):
  210. self.adb('push', local, remote)
  211. class Input(ADBWrapper):
  212. def send_keycode(self, code):
  213. try:
  214. keycode = keycodes[code]
  215. except KeyError:
  216. keycode = str(code)
  217. self.shell("input", "keyevent", keycode)
  218. def unlock_phone(self, password):
  219. if self.mode == 'recovery':
  220. return
  221. if not decode_parcel(self.shell('service', 'call', 'power', '12', output="out"), 'int'):
  222. self.send_keycode('power')
  223. if decode_parcel(self.shell('service', 'call', 'trust', '7', output="out"), 'int'):
  224. self.send_keycode('space')
  225. self.shell("input", "text", str(password))
  226. self.send_keycode('enter')
  227. class TWRP(FSActionWrapper):
  228. def backup(self, *partitions, name=None, backupdir=None):
  229. if self.mode != 'recovery':
  230. self.reboot('recovery')
  231. if backupdir is None:
  232. backupdir = config['local']['twrp']
  233. else:
  234. backupdir = backupdir
  235. options_dict = {
  236. "system": "S",
  237. "data": "D",
  238. "cache": "C",
  239. "recovery": "R",
  240. "spec_part_1": "1",
  241. "spec_part_2": "2",
  242. "spec_part_3": "3",
  243. "boot": "B",
  244. "as": "A"
  245. }
  246. options = "".join(options_dict[option] for option in partitions)
  247. if not name:
  248. name = "backup_" + \
  249. datetime.datetime.today().strftime(defaults['date_format'])
  250. filename = os.path.join(backupdir, name)
  251. self.shell("twrp", "backup", options, name)
  252. phone_dir = "/data/media/0/TWRP/BACKUPS/{serial}/{name}".format(
  253. serial=self.serial, name=name)
  254. self.move(phone_dir, filename)
  255. def wipe(self, partition):
  256. if self.mode != 'recovery':
  257. self.reboot('recovery')
  258. self.shell("twrp", "wipe", partition)
  259. def install(self, name):
  260. if self.mode != 'recovery':
  261. self.reboot('recovery')
  262. if os.path.exists(name):
  263. local_name = name
  264. name = os.path.split(name)[-1]
  265. update_path = '{}/{}'.format(config['remote']['updates'], name)
  266. if not self.exists(config['remote']['updates']):
  267. self.sudo('mkdir', config['remote']['updates'])
  268. if not self.exists(update_path):
  269. self.push(local_name, config['remote']['updates'])
  270. else:
  271. update_path = '{}/{}'.format(config['remote']['updates'], name)
  272. self.shell("twrp", "install", update_path)
  273. class Device(TWRP, Input):
  274. pass
  275. if __name__ == "__main__" and debug:
  276. d = Device.prim_device()