Dadb.kt 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. package eu.mjdev.dadb
  2. import eu.mjdev.dadb.*
  3. import eu.mjdev.dadb.adbserver.AdbServer
  4. import eu.mjdev.dadb.forwarding.TcpForwardDescriptor
  5. import eu.mjdev.dadb.forwarding.TcpForwarder
  6. import eu.mjdev.dadb.helpers.*
  7. import okio.*
  8. import java.io.File
  9. import java.nio.file.Files
  10. interface Dadb : AutoCloseable {
  11. @Throws(IOException::class)
  12. fun open(destination: String): AdbStream
  13. fun supportsFeature(feature: String): Boolean
  14. @Throws(IOException::class)
  15. fun shell(command: String): AdbShellResponse {
  16. openShell(command).use { stream ->
  17. return stream.readAll()
  18. }
  19. }
  20. @Throws(IOException::class)
  21. fun openShell(command: String = ""): AdbShellStream {
  22. val stream = open("shell,v2,raw:$command")
  23. return AdbShellStream(stream)
  24. }
  25. @Throws(IOException::class)
  26. fun push(src: File, remotePath: String, mode: Int = readMode(src), lastModifiedMs: Long = src.lastModified()) {
  27. push(src.source(), remotePath, mode, lastModifiedMs)
  28. }
  29. @Throws(IOException::class)
  30. fun push(source: Source, remotePath: String, mode: Int, lastModifiedMs: Long) {
  31. openSync().use { stream ->
  32. stream.send(source, remotePath, mode, lastModifiedMs)
  33. }
  34. }
  35. @Throws(IOException::class)
  36. fun pull(dst: File, remotePath: String) {
  37. pull(dst.sink(append = false), remotePath)
  38. }
  39. @Throws(IOException::class)
  40. fun pull(sink: Sink, remotePath: String) {
  41. openSync().use { stream ->
  42. stream.recv(sink, remotePath)
  43. }
  44. }
  45. @Throws(IOException::class)
  46. fun openSync(): AdbSyncStream {
  47. val stream = open("sync:")
  48. return AdbSyncStream(stream)
  49. }
  50. @Throws(IOException::class)
  51. fun install(file: File, vararg options: String) {
  52. if (supportsFeature("cmd")) {
  53. install(file.source(), file.length(), *options)
  54. } else {
  55. pmInstall(file, *options)
  56. }
  57. }
  58. @Throws(IOException::class)
  59. fun install(source: Source, size: Long, vararg options: String) {
  60. if (supportsFeature("cmd")) {
  61. execCmd("package", "install", "-S", size.toString(), *options).use { stream ->
  62. stream.sink.writeAll(source)
  63. stream.sink.flush()
  64. val response = stream.source.readString(Charsets.UTF_8)
  65. if (!response.startsWith("Success")) {
  66. throw IOException("Install failed: $response")
  67. }
  68. }
  69. } else {
  70. val tempFile = kotlin.io.path.createTempFile()
  71. val fileSink = tempFile.sink().buffer()
  72. fileSink.writeAll(source)
  73. fileSink.flush()
  74. pmInstall(tempFile.toFile(), *options)
  75. }
  76. }
  77. private fun pmInstall(file: File, vararg options: String) {
  78. val fileName = file.name
  79. val remotePath = "/data/local/tmp/$fileName"
  80. push(file, remotePath)
  81. shell("pm install ${options.joinToString(" ")} \"$remotePath\"")
  82. }
  83. @Throws(IOException::class)
  84. fun installMultiple(apks: List<File>, vararg options: String) {
  85. // http://aospxref.com/android-12.0.0_r3/xref/packages/modules/adb/client/adb_install.cpp#538
  86. if (supportsFeature("cmd")) {
  87. val totalLength = apks.map { it.length() }.reduce { acc, l -> acc + l }
  88. execCmd("package", "install-create", "-S", totalLength.toString(), *options).use { createStream ->
  89. val response = createStream.source.readString(Charsets.UTF_8)
  90. if (!response.startsWith("Success")) {
  91. throw IOException("connect error for create: $response")
  92. }
  93. val pattern = """\[(\w+)]""".toRegex()
  94. val sessionId =
  95. pattern.find(response)?.groups?.get(1)?.value ?: throw IOException("failed to create session")
  96. var error: String? = null
  97. apks.forEach { apk ->
  98. // install write every apk file to stream
  99. execCmd(
  100. "package",
  101. "install-write",
  102. "-S",
  103. apk.length().toString(),
  104. sessionId,
  105. apk.name,
  106. "-",
  107. *options
  108. ).use { writeStream ->
  109. writeStream.sink.writeAll(apk.source())
  110. writeStream.sink.flush()
  111. val writeResponse = writeStream.source.readString(Charsets.UTF_8)
  112. if (!writeResponse.startsWith("Success")) {
  113. error = writeResponse
  114. return@forEach
  115. }
  116. }
  117. }
  118. // commit the session
  119. val finalCommand = if (error == null) "install-commit" else "install-abandon"
  120. execCmd("package", finalCommand, sessionId, *options).use { commitStream ->
  121. val finalResponse = commitStream.source.readString(Charsets.UTF_8)
  122. if (!finalResponse.startsWith("Success")) {
  123. throw IOException("failed to finalize session: $commitStream")
  124. }
  125. }
  126. if (error != null) {
  127. throw IOException("Install failed: $error")
  128. }
  129. }
  130. } else {
  131. val totalLength = apks.map { it.length() }.reduce { acc, l -> acc + l }
  132. // step1: create session
  133. val response = shell("pm install-create -S $totalLength ${options.joinToString(" ")}")
  134. if (!response.allOutput.startsWith("Success")) {
  135. throw IOException("pm create session failed: $response")
  136. }
  137. val pattern = """\[(\w+)]""".toRegex()
  138. val sessionId =
  139. pattern.find(response.allOutput)?.groups?.get(1)?.value ?: throw IOException("failed to create session")
  140. var error: String? = null
  141. val fileNames = apks.map { it.name }
  142. val remotePaths = fileNames.map { "/data/local/tmp/$it" }
  143. // step2: write apk to the session
  144. apks.zip(remotePaths).forEachIndexed { index, pair ->
  145. val apk = pair.first
  146. val remotePath = pair.second
  147. try {
  148. // we should push the apk files to device, when push failed, it would stop the installation
  149. push(apk, remotePath)
  150. } catch (t: IOException) {
  151. error = t.message
  152. return@forEachIndexed
  153. }
  154. // pm install-write -S APK_SIZE SESSION_ID INDEX PATH
  155. val writeResponse = shell("pm install-write -S ${apk.length()} $sessionId $index $remotePath")
  156. if (!writeResponse.allOutput.startsWith("Success")) {
  157. error = writeResponse.allOutput
  158. return@forEachIndexed
  159. }
  160. }
  161. // step3: commit or abandon the session
  162. val finalCommand = if (error == null) "pm install-commit $sessionId" else "pm install-abandon $sessionId"
  163. val finalResponse = shell(finalCommand)
  164. if (!finalResponse.allOutput.startsWith("Success")) {
  165. throw IOException("failed to finalize session: $finalResponse")
  166. }
  167. if (error != null) {
  168. throw IOException("Install failed: $error");
  169. }
  170. }
  171. }
  172. @Throws(IOException::class)
  173. fun uninstall(packageName: String) {
  174. val response = shell("cmd package uninstall $packageName")
  175. if (response.exitCode != 0) {
  176. throw IOException("Uninstall failed: ${response.allOutput}")
  177. }
  178. }
  179. @Throws(IOException::class)
  180. fun execCmd(vararg command: String): AdbStream {
  181. if (!supportsFeature("cmd")) throw UnsupportedOperationException("cmd is not supported on this version of Android")
  182. val destination = (listOf("exec:cmd") + command).joinToString(" ")
  183. return open(destination)
  184. }
  185. @Throws(IOException::class)
  186. fun abbExec(vararg command: String): AdbStream {
  187. if (!supportsFeature("abb_exec")) throw UnsupportedOperationException("abb_exec is not supported on this version of Android")
  188. val destination = "abb_exec:${command.joinToString("\u0000")}"
  189. return open(destination)
  190. }
  191. @Throws(IOException::class)
  192. fun root() {
  193. val response = restartAdb(this, "root:")
  194. if (!response.startsWith("restarting") && !response.contains("already")) {
  195. throw IOException("Failed to restart adb as root: $response")
  196. }
  197. waitRootOrClose(this, root = true)
  198. }
  199. @Throws(IOException::class)
  200. fun unroot() {
  201. val response = restartAdb(this, "unroot:")
  202. if (!response.startsWith("restarting") && !response.contains("not running as root")) {
  203. throw IOException("Failed to restart adb as root: $response")
  204. }
  205. waitRootOrClose(this, root = false)
  206. }
  207. @Throws(InterruptedException::class)
  208. fun tcpForward(targetPort: Int, hostPort: Int): TcpForwardDescriptor {
  209. val forwarder = TcpForwarder(this, targetPort, hostPort)
  210. val localPort = forwarder.start()
  211. return TcpForwardDescriptor(forwarder, localPort)
  212. }
  213. @Throws(InterruptedException::class)
  214. fun tcpForward(targetPort: Int): TcpForwardDescriptor {
  215. val forwarder = TcpForwarder(this, targetPort)
  216. val localPort = forwarder.start()
  217. return TcpForwardDescriptor(forwarder, localPort)
  218. }
  219. companion object {
  220. private const val MIN_EMULATOR_PORT = 5555
  221. private const val MAX_EMULATOR_PORT = 5683
  222. @JvmStatic
  223. @JvmOverloads
  224. fun create(
  225. host: String,
  226. port: Int,
  227. keyPair: AdbKeyPair? = AdbKeyPair.readDefault(),
  228. connectTimeout: Int = 0,
  229. socketTimeout: Int = 0
  230. ): Dadb = DadbImpl(host, port, keyPair, connectTimeout, socketTimeout)
  231. @JvmStatic
  232. @JvmOverloads
  233. fun discover(
  234. host: String = "localhost",
  235. keyPair: AdbKeyPair? = AdbKeyPair.readDefault()
  236. ): List<Dadb> {
  237. return list(host, keyPair)
  238. }
  239. @JvmStatic
  240. @JvmOverloads
  241. fun list(host: String = "localhost", keyPair: AdbKeyPair? = AdbKeyPair.readDefault()): List<Dadb> {
  242. val dadbs = AdbServer.listDadbs(adbServerHost = host)
  243. if (dadbs.isNotEmpty()) return dadbs
  244. return (MIN_EMULATOR_PORT..MAX_EMULATOR_PORT).mapNotNull { port ->
  245. val dadb = create(host, port, keyPair)
  246. val response = try {
  247. dadb.shell("echo success").allOutput
  248. } catch (ignore: Throwable) {
  249. null
  250. }
  251. if (response == "success\n") {
  252. dadb
  253. } else {
  254. null
  255. }
  256. }
  257. }
  258. private fun waitRootOrClose(dadb: Dadb, root: Boolean) {
  259. while (true) {
  260. try {
  261. val response = dadb.shell("getprop service.adb.root")
  262. val propValue = if (root) 1 else 0
  263. if (response.output == "$propValue\n") return
  264. } catch (e: IOException) {
  265. return
  266. }
  267. }
  268. }
  269. private fun restartAdb(dadb: Dadb, destination: String): String {
  270. dadb.open(destination).use { stream ->
  271. return stream.source.readUntil('\n'.code.toByte()).readString(Charsets.UTF_8)
  272. }
  273. }
  274. private fun BufferedSource.readUntil(endByte: Byte): Buffer {
  275. val buffer = Buffer()
  276. while (true) {
  277. val b = readByte()
  278. buffer.writeByte(b.toInt())
  279. if (b == endByte) return buffer
  280. }
  281. }
  282. private fun readMode(file: File): Int {
  283. return Files.getAttribute(file.toPath(), "unix:mode") as? Int
  284. ?: throw RuntimeException("Unable to read file mode")
  285. }
  286. }
  287. }