IAdb.kt 12 KB

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