diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 3955f9c70..db0697406 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -9,7 +9,7 @@ jobs:
uses: actions/setup-java@v1
with:
java-version: 1.8
- - uses: malinskiy/action-android/install-sdk@release/0.0.4
+ - uses: malinskiy/action-android/install-sdk@release/0.0.6
- name: gradle test jacocoTestReport
run: ./gradlew test jacocoTestReport
- name: Save test output
@@ -38,9 +38,10 @@ jobs:
uses: actions/setup-java@v1
with:
java-version: 1.8
- - uses: malinskiy/action-android/install-sdk@release/0.0.4
+ - uses: malinskiy/action-android/install-sdk@release/0.0.6
- name: integration test
- uses: malinskiy/action-android/emulator-run-cmd@release/0.0.4
+ uses: malinskiy/action-android/emulator-run-cmd@release/0.0.6
+ timeout-minutes: 20
with:
cmd: ./gradlew integrationTest
api: ${{ matrix.api }}
@@ -59,22 +60,13 @@ jobs:
with:
name: integration-test-coverage
path: build/reports/jacoco/jacocoIntegrationTestReport/html
+ - name: Save logcat output
+ uses: actions/upload-artifact@master
+ if: failure()
+ with:
+ name: logcat
+ path: artifacts/logcat.log
- name: codecov integartion tests
run: bash <(curl -s https://codecov.io/bash) -f ./build/reports/jacoco/jacocoIntegrationTestReport/jacocoIntegrationTestReport.xml -F integration
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
- deploy:
- needs: test
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v1
- - name: Set up JDK 1.8
- uses: actions/setup-java@v1
- with:
- java-version: 1.8
- - name: deploy-snapshot
- run: bash .buildsystem/deploy-sonatype.sh
- env:
- SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }}
- SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }}
- GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
diff --git a/.github/workflows/deploy-sonatype-snapshot.yaml b/.github/workflows/deploy-sonatype-snapshot.yaml
new file mode 100644
index 000000000..237e7c072
--- /dev/null
+++ b/.github/workflows/deploy-sonatype-snapshot.yaml
@@ -0,0 +1,22 @@
+name: deploy-sonatype-snapshot
+on:
+ push:
+ branches:
+ - master
+ tags-ignore:
+ - '*'
+jobs:
+ deploy:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v1
+ - name: Set up JDK 1.8
+ uses: actions/setup-java@v1
+ with:
+ java-version: 1.8
+ - name: deploy-snapshot
+ run: bash .buildsystem/deploy-sonatype.sh
+ env:
+ SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }}
+ SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }}
+ GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index aecc2806d..c3df5ec6d 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,5 +1,8 @@
-
+
+
+
+
\ No newline at end of file
diff --git a/src/integrationTest/kotlin/com/malinskiy/adam/integration/E2ETest.kt b/src/integrationTest/kotlin/com/malinskiy/adam/integration/E2ETest.kt
index fbc82f68b..afeece166 100644
--- a/src/integrationTest/kotlin/com/malinskiy/adam/integration/E2ETest.kt
+++ b/src/integrationTest/kotlin/com/malinskiy/adam/integration/E2ETest.kt
@@ -27,7 +27,6 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.runBlocking
import org.junit.Rule
import org.junit.Test
-import java.awt.image.BufferedImage
import java.io.File
import java.io.IOException
import javax.imageio.ImageIO
@@ -44,21 +43,9 @@ class E2ETest {
val image = adbRule.adb.execute(
ScreenCaptureRequest(),
adbRule.deviceSerial
- )
-
- val finalImage = BufferedImage(image.width, image.height, BufferedImage.TYPE_INT_ARGB)
-
- var index = 0
- val increment = image.bitsPerPixel shr 3
- for (y in 0 until image.height) {
- for (x in 0 until image.width) {
- val value = image.getARGB(index)
- index += increment
- finalImage.setRGB(x, y, value)
- }
- }
+ ).toBufferedImage()
- if (!ImageIO.write(finalImage, "png", File("/tmp/screen.png"))) {
+ if (!ImageIO.write(image, "png", File("/tmp/screen.png"))) {
throw IOException("Failed to find png writer")
}
}
diff --git a/src/integrationTest/kotlin/com/malinskiy/adam/integration/FileE2ETest.kt b/src/integrationTest/kotlin/com/malinskiy/adam/integration/FileE2ETest.kt
index 52cf4585c..d0e6104fb 100644
--- a/src/integrationTest/kotlin/com/malinskiy/adam/integration/FileE2ETest.kt
+++ b/src/integrationTest/kotlin/com/malinskiy/adam/integration/FileE2ETest.kt
@@ -17,6 +17,7 @@
package com.malinskiy.adam.integration
import assertk.assertThat
+import assertk.assertions.contains
import assertk.assertions.isEqualTo
import com.malinskiy.adam.extension.md5
import com.malinskiy.adam.request.sync.PullFileRequest
@@ -27,6 +28,8 @@ import com.malinskiy.adam.rule.AdbDeviceRule
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.channels.receiveOrNull
import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withTimeout
+import org.junit.After
import org.junit.Rule
import org.junit.Test
import java.io.File
@@ -88,12 +91,33 @@ class FileE2ETest {
}
}
+ @After
+ fun cleanup() {
+ runBlocking {
+ adbRule.adb.execute(ShellCommandRequest("rm /data/local/tmp/testfile"), serial = adbRule.deviceSerial)
+ }
+ }
+
@Test
fun testFilePulling() {
runBlocking {
- val testFile = File("/tmp/build.prop")
+ val testFile = createTempFile()
+
+ withTimeout(10_000) {
+ while (true) {
+ var output =
+ adbRule.adb.execute(ShellCommandRequest("echo cafebabe > /data/local/tmp/testfile"), serial = adbRule.deviceSerial)
+ println(output)
+ output = adbRule.adb.execute(ShellCommandRequest("cat /data/local/tmp/testfile"), serial = adbRule.deviceSerial)
+ println(output)
+ if (output.contains("cafebabe")) {
+ break
+ }
+ }
+ }
+
val channel = adbRule.adb.execute(
- PullFileRequest("/system/build.prop", testFile),
+ PullFileRequest("/data/local/tmp/testfile", testFile),
GlobalScope,
adbRule.deviceSerial
)
@@ -110,7 +134,7 @@ class FileE2ETest {
}
println()
- val sizeString = adbRule.adb.execute(ShellCommandRequest("ls -ln /system/build.prop"), adbRule.deviceSerial)
+ val sizeString = adbRule.adb.execute(ShellCommandRequest("ls -ln /data/local/tmp/testfile"), adbRule.deviceSerial)
val split = sizeString.split(" ").filter { it != "" }
/**
@@ -120,6 +144,8 @@ class FileE2ETest {
*/
val dateIndex = split.indexOfLast { it.matches("[\\d]{4}-[\\d]{2}-[\\d]{2}".toRegex()) }
assertThat(split[dateIndex - 1].toLong()).isEqualTo(testFile.length())
+
+ assertThat(testFile.readText()).contains("cafebabe")
}
}
}
\ No newline at end of file
diff --git a/src/integrationTest/kotlin/com/malinskiy/adam/rule/AdbDeviceRule.kt b/src/integrationTest/kotlin/com/malinskiy/adam/rule/AdbDeviceRule.kt
index c8337e519..742f3cff6 100644
--- a/src/integrationTest/kotlin/com/malinskiy/adam/rule/AdbDeviceRule.kt
+++ b/src/integrationTest/kotlin/com/malinskiy/adam/rule/AdbDeviceRule.kt
@@ -18,20 +18,48 @@ package com.malinskiy.adam.rule
import com.malinskiy.adam.AndroidDebugBridgeClientFactory
import com.malinskiy.adam.interactor.StartAdbInteractor
+import com.malinskiy.adam.request.devices.Device
+import com.malinskiy.adam.request.devices.DeviceState
+import com.malinskiy.adam.request.devices.ListDevicesRequest
+import com.malinskiy.adam.request.sync.GetSinglePropRequest
import kotlinx.coroutines.runBlocking
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
+import java.net.ConnectException
class AdbDeviceRule : TestRule {
val deviceSerial = "emulator-5554"
val adb = AndroidDebugBridgeClientFactory().build()
override fun apply(base: Statement?, description: Description?): Statement {
- return object: Statement() {
+ return object : Statement() {
override fun evaluate() {
runBlocking {
StartAdbInteractor().execute()
+
+ //Wait for device to be available on adb server
+ while (true) {
+ try {
+ val output = adb.execute(ListDevicesRequest())
+ if (output.contains(Device(deviceSerial, DeviceState.DEVICE))) {
+ break
+ }
+ } catch (e: ConnectException) {
+ continue
+ }
+ }
+
+ //Wait for device boot
+ while (true) {
+ try {
+ val completed = adb.execute(GetSinglePropRequest("sys.boot_completed"))
+ if (completed.isNotEmpty()) break
+ continue
+ } catch (e: ConnectException) {
+ continue
+ }
+ }
}
base?.evaluate()
}
diff --git a/src/main/kotlin/com/malinskiy/adam/request/sync/PullFileRequest.kt b/src/main/kotlin/com/malinskiy/adam/request/sync/PullFileRequest.kt
index 35a3649ed..7c67574cf 100644
--- a/src/main/kotlin/com/malinskiy/adam/request/sync/PullFileRequest.kt
+++ b/src/main/kotlin/com/malinskiy/adam/request/sync/PullFileRequest.kt
@@ -109,7 +109,10 @@ class PullFileRequest(
return currentPosition.toDouble() / totalBytes
}
header.contentEquals(Const.Message.FAIL) -> {
- throw PullFailedException("Failed to pull file $remotePath")
+ val size = headerBuffer.copyOfRange(4, 8).toInt()
+ readChannel.readFully(dataBuffer, 0, size)
+ val errorMessage = String(dataBuffer, 0, size)
+ throw PullFailedException("Failed to pull file $remotePath: $errorMessage")
}
else -> {
throw UnsupportedSyncProtocolException("Unexpected header message ${String(header, Const.DEFAULT_TRANSPORT_ENCODING)}")
diff --git a/src/main/kotlin/com/malinskiy/adam/request/sync/RawImage.kt b/src/main/kotlin/com/malinskiy/adam/request/sync/RawImage.kt
index 97102e826..b5d3d0533 100644
--- a/src/main/kotlin/com/malinskiy/adam/request/sync/RawImage.kt
+++ b/src/main/kotlin/com/malinskiy/adam/request/sync/RawImage.kt
@@ -17,12 +17,20 @@
package com.malinskiy.adam.request.sync
import com.malinskiy.adam.exception.UnsupportedImageProtocolException
+import java.awt.color.ICC_ColorSpace
+import java.awt.color.ICC_Profile
import java.awt.image.BufferedImage
+import java.awt.image.ColorModel
+import java.awt.image.DataBuffer
+import java.awt.image.DirectColorModel
+import java.io.IOException
import java.nio.ByteBuffer
+
data class RawImage(
val version: Int,
val bitsPerPixel: Int,
+ val colorSpace: ColorSpace? = null,
val size: Int,
val width: Int,
val height: Int,
@@ -43,26 +51,30 @@ data class RawImage(
val g: Int
val b: Int
val a: Int
- if (bitsPerPixel == 16) {
- value = buffer[index].toInt() and 0x00FF
- value = value or (buffer[index + 1].toInt() shl 8 and 0x0FF00)
- // RGB565 to RGB888
- // Multiply by 255/31 to convert from 5 bits (31 max) to 8 bits (255)
- r = (value.ushr(11) and 0x1f) * 255 / 31
- g = (value.ushr(5) and 0x3f) * 255 / 63
- b = (value and 0x1f) * 255 / 31
- a = 0xFF // force alpha to opaque if there's no alpha value in the framebuffer.
- } else if (bitsPerPixel == 32) {
- value = buffer[index].toInt() and 0x00FF
- value = value or (buffer[index + 1].toInt() and 0x00FF shl 8)
- value = value or (buffer[index + 2].toInt() and 0x00FF shl 16)
- value = value or (buffer[index + 3].toInt() and 0x00FF shl 24)
- r = value.ushr(redOffset) and getMask(redLength) shl 8 - redLength
- g = value.ushr(greenOffset) and getMask(greenLength) shl 8 - greenLength
- b = value.ushr(blueOffset) and getMask(blueLength) shl 8 - blueLength
- a = value.ushr(alphaOffset) and getMask(alphaLength) shl 8 - alphaLength
- } else {
- throw UnsupportedOperationException("RawImage.getARGB(int) only works in 16 and 32 bit mode.")
+ when (bitsPerPixel) {
+ 16 -> {
+ value = buffer[index].toInt() and 0x00FF
+ value = value or (buffer[index + 1].toInt() shl 8 and 0x0FF00)
+ // RGB565 to RGB888
+ // Multiply by 255/31 to convert from 5 bits (31 max) to 8 bits (255)
+ r = (value.ushr(11) and 0x1f) * 255 / 31
+ g = (value.ushr(5) and 0x3f) * 255 / 63
+ b = (value and 0x1f) * 255 / 31
+ a = 0xFF // force alpha to opaque if there's no alpha value in the framebuffer.
+ }
+ 32 -> {
+ value = buffer[index].toInt() and 0x00FF
+ value = value or (buffer[index + 1].toInt() and 0x00FF shl 8)
+ value = value or (buffer[index + 2].toInt() and 0x00FF shl 16)
+ value = value or (buffer[index + 3].toInt() and 0x00FF shl 24)
+ r = value.ushr(redOffset) and getMask(redLength) shl 8 - redLength
+ g = value.ushr(greenOffset) and getMask(greenLength) shl 8 - greenLength
+ b = value.ushr(blueOffset) and getMask(blueLength) shl 8 - blueLength
+ a = value.ushr(alphaOffset) and getMask(alphaLength) shl 8 - alphaLength
+ }
+ else -> {
+ throw UnsupportedOperationException("RawImage.getARGB(int) only works in 16 and 32 bit mode.")
+ }
}
return a shl 24 or (r shl 16) or (g shl 8) or b
@@ -73,18 +85,44 @@ data class RawImage(
}
fun toBufferedImage(): BufferedImage {
- val image =
- BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB)
+ val profileName = getProfileName()
+ val bufferedImage = when (profileName) {
+ null -> {
+ BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB)
+ }
+ else -> {
+ var profile: ICC_Profile? = ICC_Profile.getInstance(ICC_ColorSpace.CS_sRGB)
+ try {
+ profile = ICC_Profile.getInstance(javaClass.classLoader.getResourceAsStream("colorProfiles/$profileName"))
+ } catch (e: IOException) { // Ignore
+ }
+ val colorSpace = ICC_ColorSpace(profile)
+
+ val colorModel: ColorModel =
+ DirectColorModel(colorSpace, 32, 0x00ff0000, 0x0000ff00, 0x000000ff, -0x1000000, false, DataBuffer.TYPE_INT)
+ val raster = colorModel.createCompatibleWritableRaster(width, height)
+
+ BufferedImage(colorModel, raster, colorModel.isAlphaPremultiplied, null)
+ }
+ }
- var index = 0
val bytesPerPixel = bitsPerPixel shr 3
for (y in 0 until height) {
for (x in 0 until width) {
- image.setRGB(x, y, getARGB(index) or -0x1000000)
- index += bytesPerPixel
+ bufferedImage.setRGB(x, y, getARGB((x + y * width) * bytesPerPixel))
}
}
- return image
+
+ return bufferedImage
+ }
+
+ private fun getProfileName(): String? {
+ when (colorSpace) {
+ ColorSpace.UNKNOWN -> return null
+ ColorSpace.SRGB -> return "sRGB.icc"
+ ColorSpace.P3 -> return "DisplayP3.icc"
+ }
+ return null
}
companion object {
@@ -130,9 +168,40 @@ data class RawImage(
alphaLength = bytes.int,
buffer = imageBuffer.moveToByteArray()
)
+ 2 -> RawImage(
+ version = version,
+ bitsPerPixel = bytes.int,
+ colorSpace = ColorSpace.from(bytes.int),
+ size = bytes.int,
+ width = bytes.int,
+ height = bytes.int,
+ redOffset = bytes.int,
+ redLength = bytes.int,
+ blueOffset = bytes.int,
+ blueLength = bytes.int,
+ greenOffset = bytes.int,
+ greenLength = bytes.int,
+ alphaOffset = bytes.int,
+ alphaLength = bytes.int,
+ buffer = imageBuffer.moveToByteArray()
+ )
else -> throw UnsupportedImageProtocolException(version)
}
}
}
+}
+
+enum class ColorSpace {
+ UNKNOWN,
+ SRGB,
+ P3;
+
+ companion object {
+ fun from(value: Int) = when (value) {
+ 1 -> SRGB
+ 2 -> P3
+ else -> UNKNOWN
+ }
+ }
}
\ No newline at end of file
diff --git a/src/main/kotlin/com/malinskiy/adam/request/sync/ScreenCaptureRequest.kt b/src/main/kotlin/com/malinskiy/adam/request/sync/ScreenCaptureRequest.kt
index 2a6be4ef9..78f9e001e 100644
--- a/src/main/kotlin/com/malinskiy/adam/request/sync/ScreenCaptureRequest.kt
+++ b/src/main/kotlin/com/malinskiy/adam/request/sync/ScreenCaptureRequest.kt
@@ -32,6 +32,7 @@ class ScreenCaptureRequest : ComplexRequest() {
val protocolVersion = protocolBuffer.order(ByteOrder.LITTLE_ENDIAN).int
val headerSize = when (protocolVersion) {
1 -> 12 // bpp, size, width, height, 4*(length, offset)
+ 2 -> 13 // bpp, colorSpace, size, width, height, 4*(length, offset)
16 -> 3 // compatibility mode: size, width, height. used previously to denote framebuffer depth
else -> throw UnsupportedImageProtocolException(protocolVersion)
}
@@ -42,7 +43,13 @@ class ScreenCaptureRequest : ComplexRequest() {
headerBuffer.order(ByteOrder.LITTLE_ENDIAN)
headerBuffer.rewind()
- val imageSize = headerBuffer.getInt(4)
+ val imageSize = when (protocolVersion) {
+ 1 -> headerBuffer.getInt(4)
+ 2 -> headerBuffer.getInt(8)
+ 16 -> headerBuffer.getInt(0)
+ else -> throw UnsupportedImageProtocolException(protocolVersion)
+ }
+
val imageBuffer = ByteBuffer.allocate(imageSize)
headerBuffer.rewind()
readChannel.readFully(imageBuffer)
diff --git a/src/main/resources/colorProfiles/DisplayP3.icc b/src/main/resources/colorProfiles/DisplayP3.icc
new file mode 100644
index 000000000..3f0126bef
Binary files /dev/null and b/src/main/resources/colorProfiles/DisplayP3.icc differ
diff --git a/src/main/resources/colorProfiles/sRGB.icc b/src/main/resources/colorProfiles/sRGB.icc
new file mode 100644
index 000000000..7f9d18d09
Binary files /dev/null and b/src/main/resources/colorProfiles/sRGB.icc differ
diff --git a/src/test/kotlin/com/malinskiy/adam/request/sync/PullFileRequestTest.kt b/src/test/kotlin/com/malinskiy/adam/request/sync/PullFileRequestTest.kt
index 358d1ff83..f67cb2565 100644
--- a/src/test/kotlin/com/malinskiy/adam/request/sync/PullFileRequestTest.kt
+++ b/src/test/kotlin/com/malinskiy/adam/request/sync/PullFileRequestTest.kt
@@ -22,6 +22,7 @@ import com.malinskiy.adam.Const
import com.malinskiy.adam.exception.PullFailedException
import com.malinskiy.adam.exception.UnsupportedSyncProtocolException
import com.malinskiy.adam.server.AndroidDebugBridgeServer
+import io.ktor.utils.io.writeIntLittleEndian
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.receiveOrNull
@@ -160,7 +161,9 @@ class PullFileRequestTest : CoroutineScope {
assertThat(recvPath).isEqualTo("/sdcard/testfile")
output.respond(Const.Message.FAIL)
- output.respondDone()
+ val message = "lorem ipsum"
+ output.writeIntLittleEndian(message.length)
+ output.respondData(message.toByteArray(Const.DEFAULT_TRANSPORT_ENCODING))
}
val tempFile = createTempFile()