Skip to content

Commit

Permalink
Support installing Internal Sharing artifacts (#732)
Browse files Browse the repository at this point in the history
Signed-off-by: Alex Saveau <[email protected]>
  • Loading branch information
SUPERCILEX authored Oct 30, 2019
1 parent 8a3938e commit 07dc011
Show file tree
Hide file tree
Showing 8 changed files with 300 additions and 3 deletions.
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,15 @@ Run `./gradlew uploadReleasePrivateBundle` for App Bundles and `./gradlew upload
for APKs. To upload an existing artifact, read about
[how to do so](#uploading-a-pre-existing-artifact).

#### Installing Internal Sharing artifacts

To accelerate development, GPP supports uploading and then immediately installing Internal Sharing
artifacts. This is similar to the AGP's `install[Variant]` task.

Run `./gradlew installReleasePrivateArtifact` to install an artifact built on-the-fly and
`./gradlew uploadReleasePrivateBundle --artifact-dir path/to/artifact installReleasePrivateArtifact`
to install an existing artifact.

### Promoting artifacts

Existing releases can be promoted and/or updated to the [configured track](#common-configuration)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import com.github.triplet.gradle.play.internal.validateCreds
import com.github.triplet.gradle.play.internal.validateDebuggability
import com.github.triplet.gradle.play.tasks.Bootstrap
import com.github.triplet.gradle.play.tasks.GenerateResources
import com.github.triplet.gradle.play.tasks.InstallInternalSharingArtifact
import com.github.triplet.gradle.play.tasks.ProcessArtifactMetadata
import com.github.triplet.gradle.play.tasks.PromoteRelease
import com.github.triplet.gradle.play.tasks.PublishApk
Expand Down Expand Up @@ -237,7 +238,7 @@ internal class PlayPublisherPlugin : Plugin<Project> {
doFirst { logger.warn("$name is deprecated, use ${publishApkTask.get().name} instead") }
}

project.newTask<PublishInternalSharingApk>(
val publishInternalSharingApkTask = project.newTask<PublishInternalSharingApk>(
"upload${variantName}PrivateApk",
"Uploads Internal Sharing APK for variant '$name'. See " +
"https://github.com/Triple-T/gradle-play-publisher#uploading-an-internal-sharing-artifact",
Expand Down Expand Up @@ -281,7 +282,7 @@ internal class PlayPublisherPlugin : Plugin<Project> {
commitEditTask { mustRunAfter(publishBundleTask) }
publishBundleAllTask { dependsOn(publishBundleTask) }

project.newTask<PublishInternalSharingBundle>(
val publishInternalSharingBundleTask = project.newTask<PublishInternalSharingBundle>(
"upload${variantName}PrivateBundle",
"Uploads Internal Sharing App Bundle for variant '$name'. See " +
"https://github.com/Triple-T/gradle-play-publisher#uploading-an-internal-sharing-artifact",
Expand Down Expand Up @@ -322,6 +323,20 @@ internal class PlayPublisherPlugin : Plugin<Project> {
dependsOn(publishProductsTask)
}
publishAllTask { dependsOn(publishTask) }

val installTask = project.newTask<InstallInternalSharingArtifact>(
"install${variantName}PrivateArtifact",
"Launches an intent to install an Internal Sharing artifact for variant " +
"'$name'. See " +
"https://github.com/Triple-T/gradle-play-publisher#installing-internal-sharing-artifacts",
arrayOf(android)
) {
uploadedArtifacts.set(if (extension.defaultToAppBundles) {
publishInternalSharingBundleTask.flatMap { it.outputDirectory }
} else {
publishInternalSharingApkTask.flatMap { it.outputDirectory }
})
}
}
}
}
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())
}
}
}
}
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>
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 {}
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()
}
}
3 changes: 2 additions & 1 deletion testapp-setup.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ private val publicGppTasks = listOf(
"publishReleaseListing",
"publishReleaseProducts",
"uploadReleasePrivateApk",
"uploadReleasePrivateBundle"
"uploadReleasePrivateBundle",
"installReleasePrivateArtifact"
)

for (name in publicGppTasks) {
Expand Down
1 change: 1 addition & 0 deletions testapp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -82,5 +82,6 @@ configure<PlayPublisherExtension> {
dependencies {
"implementation"(kotlin("stdlib-jdk8", embeddedKotlinVersion))
"implementation"("androidx.appcompat:appcompat:1.1.0")
"implementation"("androidx.multidex:multidex:2.0.1")
"implementation"("androidx.constraintlayout:constraintlayout:1.1.3")
}

0 comments on commit 07dc011

Please sign in to comment.