-
-
Notifications
You must be signed in to change notification settings - Fork 341
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Support installing Internal Sharing artifacts (#732)
Signed-off-by: Alex Saveau <[email protected]>
- Loading branch information
1 parent
8a3938e
commit 07dc011
Showing
8 changed files
with
300 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
130 changes: 130 additions & 0 deletions
130
...in/src/main/kotlin/com/github/triplet/gradle/play/tasks/InstallInternalSharingArtifact.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
package com.github.triplet.gradle.play.tasks | ||
|
||
import com.android.build.gradle.AppExtension | ||
import com.android.build.gradle.internal.LoggerWrapper | ||
import com.android.builder.testing.ConnectedDeviceProvider | ||
import com.android.builder.testing.api.DeviceProvider | ||
import com.android.ddmlib.MultiLineReceiver | ||
import com.google.api.client.json.jackson2.JacksonFactory | ||
import com.google.common.annotations.VisibleForTesting | ||
import org.gradle.api.DefaultTask | ||
import org.gradle.api.file.DirectoryProperty | ||
import org.gradle.api.logging.Logging | ||
import org.gradle.api.tasks.InputDirectory | ||
import org.gradle.api.tasks.PathSensitive | ||
import org.gradle.api.tasks.PathSensitivity | ||
import org.gradle.api.tasks.TaskAction | ||
import java.util.concurrent.ExecutionException | ||
import java.util.concurrent.TimeUnit | ||
import javax.inject.Inject | ||
|
||
internal abstract class InstallInternalSharingArtifact @Inject constructor( | ||
private val extension: AppExtension | ||
) : DefaultTask() { | ||
@get:PathSensitive(PathSensitivity.RELATIVE) | ||
@get:InputDirectory | ||
abstract val uploadedArtifacts: DirectoryProperty | ||
|
||
init { | ||
// Always out-of-date since we don't know anything about the target device | ||
outputs.upToDateWhen { false } | ||
} | ||
|
||
@TaskAction | ||
fun install() { | ||
val uploads = uploadedArtifacts.get().asFileTree | ||
val latestUpload = checkNotNull(uploads.maxBy { it.nameWithoutExtension.toLong() }) { | ||
"Failed to find uploaded artifacts in ${uploads.joinToString()}" | ||
} | ||
val launchUrl = latestUpload.inputStream().use { | ||
JacksonFactory.getDefaultInstance().createJsonParser(it).parse(Map::class.java) | ||
}["downloadUrl"] as String | ||
|
||
val shell = AdbShell(extension) | ||
val result = shell.executeShellCommand( | ||
"am start -a \"android.intent.action.VIEW\" -d $launchUrl") | ||
check(result) { | ||
"Failed to install on any devices." | ||
} | ||
} | ||
|
||
interface AdbShell { | ||
fun executeShellCommand(command: String): Boolean | ||
|
||
interface Factory { | ||
fun create(extension: AppExtension): AdbShell | ||
} | ||
|
||
companion object { | ||
private var factory: Factory = DefaultAdbShell | ||
|
||
@VisibleForTesting | ||
fun setFactory(factory: Factory) { | ||
Companion.factory = factory | ||
} | ||
|
||
operator fun invoke( | ||
extension: AppExtension | ||
): AdbShell = factory.create(extension) | ||
} | ||
} | ||
|
||
private class DefaultAdbShell( | ||
private val deviceProvider: DeviceProvider, | ||
private val timeOutInMs: Long | ||
) : AdbShell { | ||
override fun executeShellCommand(command: String): Boolean { | ||
// TODO(#708): employ the #use method instead when AGP 3.6 is the minimum | ||
deviceProvider.init() | ||
return try { | ||
try { | ||
launchIntents(deviceProvider, command) | ||
} catch (e: Exception) { | ||
throw ExecutionException(e) | ||
} | ||
} finally { | ||
deviceProvider.terminate() | ||
} | ||
} | ||
|
||
private fun launchIntents(deviceProvider: DeviceProvider, command: String): Boolean { | ||
var successfulLaunches = 0 | ||
for (device in deviceProvider.devices) { | ||
val receiver = object : MultiLineReceiver() { | ||
private var _hasErrored = false | ||
val hasErrored get() = _hasErrored | ||
|
||
override fun processNewLines(lines: Array<out String>) { | ||
if (lines.any { it.contains("error", true) }) { | ||
_hasErrored = true | ||
} | ||
} | ||
|
||
override fun isCancelled() = false | ||
} | ||
|
||
device.executeShellCommand( | ||
command, | ||
receiver, | ||
timeOutInMs, | ||
TimeUnit.MILLISECONDS | ||
) | ||
|
||
if (!receiver.hasErrored) successfulLaunches++ | ||
} | ||
|
||
return successfulLaunches > 0 | ||
} | ||
|
||
companion object : AdbShell.Factory { | ||
override fun create(extension: AppExtension): AdbShell { | ||
val deviceProvider = ConnectedDeviceProvider( | ||
extension.adbExecutable, | ||
extension.adbOptions.timeOutInMs, | ||
LoggerWrapper(Logging.getLogger(InstallInternalSharingArtifact::class.java)) | ||
) | ||
return DefaultAdbShell(deviceProvider, extension.adbOptions.timeOutInMs.toLong()) | ||
} | ||
} | ||
} | ||
} |
10 changes: 10 additions & 0 deletions
10
.../test/fixtures/InstallInternalSharingArtifactIntegrationTest/src/main/AndroidManifest.xml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
<?xml version="1.0" encoding="utf-8"?> | ||
<manifest | ||
xmlns:android="http://schemas.android.com/apk/res/android" | ||
package="com.example.publisher"> | ||
|
||
<application> | ||
<activity android:name=".MainActivity" /> | ||
</application> | ||
|
||
</manifest> |
5 changes: 5 additions & 0 deletions
5
...ernalSharingArtifactIntegrationTest/src/main/java/com/example/publisher/MainActivity.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
package com.example.publisher; | ||
|
||
import android.app.Activity; | ||
|
||
public final class MainActivity extends Activity {} |
126 changes: 126 additions & 0 deletions
126
...lin/com/github/triplet/gradle/play/tasks/InstallInternalSharingArtifactIntegrationTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
package com.github.triplet.gradle.play.tasks | ||
|
||
import com.android.build.gradle.AppExtension | ||
import com.github.triplet.gradle.androidpublisher.UploadInternalSharingArtifactResponse | ||
import com.github.triplet.gradle.play.helpers.FakePlayPublisher | ||
import com.github.triplet.gradle.play.helpers.IntegrationTestBase | ||
import com.google.common.truth.Truth.assertThat | ||
import org.gradle.testkit.runner.TaskOutcome | ||
import org.junit.Test | ||
import java.io.File | ||
|
||
class InstallInternalSharingArtifactIntegrationTest : IntegrationTestBase() { | ||
@Test | ||
fun `Build depends on uploading apk artifact by default`() { | ||
@Suppress("UnnecessaryQualifiedReference") | ||
// language=gradle | ||
val config = """ | ||
com.github.triplet.gradle.play.tasks.InstallInternalSharingArtifactBridge.installFactories() | ||
""" | ||
|
||
val result = execute(config, "installReleasePrivateArtifact") | ||
|
||
assertThat(result.task(":uploadReleasePrivateApk")).isNotNull() | ||
assertThat(result.task(":uploadReleasePrivateApk")!!.outcome).isEqualTo(TaskOutcome.SUCCESS) | ||
} | ||
|
||
@Test | ||
fun `Build depends on uploading bundle artifact when specified`() { | ||
@Suppress("UnnecessaryQualifiedReference") | ||
// language=gradle | ||
val config = """ | ||
com.github.triplet.gradle.play.tasks.InstallInternalSharingArtifactBridge.installFactories() | ||
play.defaultToAppBundles true | ||
""" | ||
|
||
val result = execute(config, "installReleasePrivateArtifact") | ||
|
||
assertThat(result.task(":uploadReleasePrivateBundle")).isNotNull() | ||
assertThat(result.task(":uploadReleasePrivateBundle")!!.outcome).isEqualTo(TaskOutcome.SUCCESS) | ||
} | ||
|
||
@Test | ||
fun `Task is not cacheable`() { | ||
@Suppress("UnnecessaryQualifiedReference") | ||
// language=gradle | ||
val config = """ | ||
com.github.triplet.gradle.play.tasks.InstallInternalSharingArtifactBridge.installFactories() | ||
""" | ||
|
||
val result1 = execute(config, "installReleasePrivateArtifact") | ||
val result2 = execute(config, "installReleasePrivateArtifact") | ||
|
||
assertThat(result1.task(":installReleasePrivateArtifact")).isNotNull() | ||
assertThat(result1.task(":installReleasePrivateArtifact")!!.outcome).isEqualTo(TaskOutcome.SUCCESS) | ||
assertThat(result2.task(":installReleasePrivateArtifact")).isNotNull() | ||
assertThat(result2.task(":installReleasePrivateArtifact")!!.outcome).isEqualTo(TaskOutcome.SUCCESS) | ||
} | ||
|
||
@Test | ||
fun `Task launches view intent with artifact URL`() { | ||
@Suppress("UnnecessaryQualifiedReference") | ||
// language=gradle | ||
val config = """ | ||
com.github.triplet.gradle.play.tasks.InstallInternalSharingArtifactBridge.installFactories() | ||
""" | ||
|
||
val result = execute(config, "installReleasePrivateArtifact") | ||
|
||
assertThat(result.task(":installReleasePrivateArtifact")).isNotNull() | ||
assertThat(result.task(":installReleasePrivateArtifact")!!.outcome).isEqualTo(TaskOutcome.SUCCESS) | ||
assertThat(result.output) | ||
.contains("am start -a \"android.intent.action.VIEW\" -d myDownloadUrl") | ||
} | ||
|
||
@Test | ||
fun `Task fails when shell connection fails`() { | ||
@Suppress("UnnecessaryQualifiedReference") | ||
// language=gradle | ||
val config = """ | ||
com.github.triplet.gradle.play.tasks.InstallInternalSharingArtifactBridge.installFactories() | ||
System.setProperty("FAIL", "true") | ||
""" | ||
|
||
val result = executeExpectingFailure(config, "installReleasePrivateArtifact") | ||
|
||
assertThat(result.task(":installReleasePrivateArtifact")).isNotNull() | ||
assertThat(result.task(":installReleasePrivateArtifact")!!.outcome).isEqualTo(TaskOutcome.FAILED) | ||
assertThat(result.output).contains("Failed to install") | ||
} | ||
} | ||
|
||
object InstallInternalSharingArtifactBridge { | ||
@JvmStatic | ||
fun installFactories() { | ||
val publisher = object : FakePlayPublisher() { | ||
override fun uploadInternalSharingApk(apkFile: File): UploadInternalSharingArtifactResponse { | ||
println("uploadInternalSharingApk($apkFile)") | ||
return UploadInternalSharingArtifactResponse("{\"downloadUrl\": \"myDownloadUrl\"}", "") | ||
} | ||
|
||
override fun uploadInternalSharingBundle(bundleFile: File): UploadInternalSharingArtifactResponse { | ||
println("uploadInternalSharingBundle($bundleFile)") | ||
return UploadInternalSharingArtifactResponse("{\"downloadUrl\": \"myDownloadUrl\"}", "") | ||
} | ||
} | ||
val shell = object : InstallInternalSharingArtifact.AdbShell { | ||
fun install() { | ||
val context = this | ||
InstallInternalSharingArtifact.AdbShell.setFactory( | ||
object : InstallInternalSharingArtifact.AdbShell.Factory { | ||
override fun create(extension: AppExtension) = context | ||
}) | ||
} | ||
|
||
override fun executeShellCommand(command: String): Boolean { | ||
println("executeShellCommand($command)") | ||
return System.getProperty("FAIL") == null | ||
} | ||
} | ||
|
||
publisher.install() | ||
shell.install() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters