Skip to content

Instantly share code, notes, and snippets.

@SaschaZ
Last active November 2, 2019 04:13
Show Gist options
  • Save SaschaZ/4deeadbf72318a5451be1c9fa99940d5 to your computer and use it in GitHub Desktop.
Save SaschaZ/4deeadbf72318a5451be1c9fa99940d5 to your computer and use it in GitHub Desktop.
#!/usr/bin/env kscript
//DEPS org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0
import java.io.File
import java.util.concurrent.TimeUnit
import java.io.IOException
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineScope
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import kotlinx.coroutines.Dispatchers
object Bash {
suspend fun runCommand(
command: String,
workingDir: File = File("."),
timeoutMinutes: Long = Long.MAX_VALUE shl 2
): String? {
try {
val parts = command.split("\\s".toRegex())
val proc = ProcessBuilder(*parts.toTypedArray())
.directory(workingDir)
.redirectOutput(ProcessBuilder.Redirect.PIPE)
.redirectError(ProcessBuilder.Redirect.PIPE)
return suspendCoroutine<Process> {
it.resume<Process>(proc.start().also { it.waitFor(timeoutMinutes, TimeUnit.MINUTES) })
}.inputStream.bufferedReader().readText()
} catch (e: IOException) {
e.printStackTrace()
return null
}
}
}
object AndroidDeviceIdProvider : CoroutineScope {
override val coroutineContext = Dispatchers.IO + Job()
suspend fun provide(
pollingInterval: Long = 5000L,
listener: suspend (deviceId: String, add: Boolean) -> Unit
) {
val reportedDevices = ArrayList<String>()
while (isActive) {
readDeviceIds()?.also { latestDevices ->
val devicesToAdd = latestDevices.filter { !reportedDevices.contains(it) }
devicesToAdd.forEach {
println("new device id $it")
listener(it, true)
reportedDevices.add(it)
}
val devicesToRemove = reportedDevices.filter { !latestDevices.contains(it) }
devicesToRemove.forEach {
println("removed device id $it")
listener(it, false)
reportedDevices.remove(it)
}
}
delay(pollingInterval)
}
}
private suspend fun readDeviceIds(): List<String>? =
Bash.runCommand("adb devices")?.let { devicesOutput ->
if (devicesOutput.isBlank()) null
else {
val nonBlankLines = devicesOutput.split("\n").filter { it.isNotBlank() }
val firstWordsOfLines = nonBlankLines.map { it.split("\t").first() }.filter { it.isNotBlank() }
(1..firstWordsOfLines.lastIndex).map { firstWordsOfLines[it] }
}
} ?: null
}
object ScrCpyStarter : CoroutineScope {
override val coroutineContext = Dispatchers.IO + Job()
private var recentStart = 0L
private val startMutex = Mutex()
private val idJobMap = HashMap<String, Job>()
suspend fun runScrCpy(device: String) {
idJobMap[device] = launch(Dispatchers.IO) {
while (isActive) {
scrCpy(device)
}
}
}
fun cancelSrcCpy(device: String) = idJobMap[device]?.cancel()
private suspend fun scrCpy(device: String): String? {
startMutex.withLock {
val timeDiff = System.currentTimeMillis() - recentStart
if (timeDiff < 5000) delay(timeDiff.toLong())
recentStart = System.currentTimeMillis()
}
return Bash.runCommand("scrcpy -s $device")
}
}
runBlocking {
AndroidDeviceIdProvider.provide { id, add -> ScrCpyStarter.run { if (add) runScrCpy(id) else cancelSrcCpy(id) } }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment