summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMartynas Petuška <petuska@google.com>2023-10-03 12:29:04 +0000
committerMartynas Petuška <petuska@google.com>2023-10-03 14:26:01 +0000
commit037c6b4c9f1455ad138c43f76ceb7fbf0e26bae8 (patch)
treed7b76defba70bb722b8370621d7c5c17504b16b6
parent6a5e800fa2f88bb970ab7639419a95f6a4196b80 (diff)
downloadandroid_onboarding-037c6b4c9f1455ad138c43f76ceb7fbf0e26bae8.tar.gz
external/android_onboarding: Copybara import of Android Onboarding
Squash update. PiperOrigin-RevId: 570356198 Change-Id: Iad8ad832f702dea94598979f0b00eb46e4d0d05d
-rw-r--r--Android.bp4
-rw-r--r--src/com/android/onboarding/common/Android.bp18
-rw-r--r--src/com/android/onboarding/common/IntentWriter.kt66
-rw-r--r--src/com/android/onboarding/common/OnboardingContext.kt10
-rw-r--r--src/com/android/onboarding/contracts/Android.bp1
-rw-r--r--src/com/android/onboarding/contracts/ArgumentValidator.kt27
-rw-r--r--src/com/android/onboarding/contracts/ContractResult.kt7
-rw-r--r--src/com/android/onboarding/contracts/IntentSerializer.kt139
-rw-r--r--src/com/android/onboarding/contracts/OnboardingActivityApiContract.kt10
-rw-r--r--src/com/android/onboarding/contracts/provisioning/ACTIONS.kt8
-rw-r--r--src/com/android/onboarding/contracts/provisioning/Android.bp3
-rw-r--r--src/com/android/onboarding/contracts/provisioning/ArgumentSerializer.kt70
-rw-r--r--src/com/android/onboarding/contracts/provisioning/EXTRAS.kt4
-rw-r--r--src/com/android/onboarding/contracts/provisioning/FLAGS.kt11
-rw-r--r--src/com/android/onboarding/contracts/provisioning/FinalizationInsideSuwContract.kt45
-rw-r--r--src/com/android/onboarding/contracts/provisioning/managed/profile/Android.bp3
-rw-r--r--src/com/android/onboarding/contracts/provisioning/managed/profile/ProvisionManagedProfileArguments.kt16
-rw-r--r--src/com/android/onboarding/contracts/provisioning/managed/profile/ProvisionManagedProfileArgumentsSerializer.kt51
-rw-r--r--src/com/android/onboarding/contracts/provisioning/managed/profile/ProvisionManagedProfileContract.kt6
-rw-r--r--src/com/android/onboarding/contracts/provisioning/suw/SuwArguments.kt22
-rw-r--r--src/com/android/onboarding/contracts/provisioning/suw/SuwArgumentsSerializer.kt56
-rw-r--r--src/com/android/onboarding/contracts/setupwizard/Android.bp (renamed from src/com/android/onboarding/contracts/provisioning/suw/Android.bp)6
-rw-r--r--src/com/android/onboarding/contracts/setupwizard/SuwArguments.kt36
-rw-r--r--src/com/android/onboarding/contracts/setupwizard/SuwArgumentsSerializer.kt39
-rw-r--r--src/com/android/onboarding/flags/Android.bp3
-rw-r--r--src/com/android/onboarding/flags/DefaultOnboardingFlagsProvider.kt46
-rw-r--r--src/com/android/onboarding/flags/OnboardingFlagsModule.kt12
-rw-r--r--src/com/android/onboarding/flags/OnboardingFlagsProvider.kt7
-rw-r--r--src/com/android/onboarding/flags/testing/FakeOnboardingFlagsProvider.kt8
-rw-r--r--src/com/android/onboarding/nodes/OnboardingGraph.kt7
-rw-r--r--src/com/android/onboarding/nodes/decoder/Base64ToProtoString.kt19
-rw-r--r--src/com/android/onboarding/nodes/testing/testapp/AndroidManifest.xml48
-rw-r--r--src/com/android/onboarding/nodes/testing/testapp/MainActivity.kt103
-rw-r--r--src/com/android/onboarding/nodes/testing/testapp/res/layout/activity_main.xml31
-rw-r--r--src/com/android/onboarding/nodes/visualiser/Android.bp22
-rw-r--r--src/com/android/onboarding/nodes/visualiser/PngOnboardingGraphVisualiser.kt266
-rw-r--r--src/com/android/onboarding/testing/Android.bp18
-rw-r--r--src/com/android/onboarding/testing/ErrorSubject.kt63
38 files changed, 794 insertions, 517 deletions
diff --git a/Android.bp b/Android.bp
index 4c4e4ea..6014218 100644
--- a/Android.bp
+++ b/Android.bp
@@ -14,12 +14,13 @@ android_library {
manifest: ":android_onboarding.AndroidManifest",
dont_merge_manifests: true,
static_libs: [
+ "android_onboarding.common",
"android_onboarding.contracts",
"android_onboarding.contracts.annotations",
"android_onboarding.contracts.fragment",
"android_onboarding.contracts.provisioning",
"android_onboarding.contracts.provisioning.managed.profile",
- "android_onboarding.contracts.provisioning.suw",
+ "android_onboarding.contracts.setupwizard",
"android_onboarding.flags",
"android_onboarding.nodes",
"android_onboarding.versions",
@@ -34,5 +35,6 @@ android_library {
"android_onboarding.contracts.testing",
"android_onboarding.nodes.testing",
"android_onboarding.flags.testing",
+ "android_onboarding.testing",
],
}
diff --git a/src/com/android/onboarding/common/Android.bp b/src/com/android/onboarding/common/Android.bp
new file mode 100644
index 0000000..206168e
--- /dev/null
+++ b/src/com/android/onboarding/common/Android.bp
@@ -0,0 +1,18 @@
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_library {
+ name: "android_onboarding.common",
+ manifest: ":android_onboarding.AndroidManifest",
+ srcs: [
+ "*.kt",
+ ],
+ dont_merge_manifests: true,
+ static_libs: [
+ "dagger2",
+ "jsr330",
+ "androidx.annotation_annotation",
+ ],
+ plugins: ["dagger2-compiler"]
+}
diff --git a/src/com/android/onboarding/common/IntentWriter.kt b/src/com/android/onboarding/common/IntentWriter.kt
new file mode 100644
index 0000000..8eb0eb0
--- /dev/null
+++ b/src/com/android/onboarding/common/IntentWriter.kt
@@ -0,0 +1,66 @@
+package com.android.onboarding.common
+
+import android.content.Intent
+import android.os.Build.VERSION_CODES
+import android.os.Bundle
+import android.os.Parcelable
+import androidx.annotation.RequiresApi
+import java.io.Serializable
+
+/** Utility object providing scoped DSL utilities for concise intent extra manipulation */
+@Suppress("MemberVisibilityCanBePrivate")
+object IntentWriter {
+ fun Intent.intExtraOrNull(name: String): Int? = if (hasExtra(name)) getIntExtra(name, 0) else null
+
+ fun Intent.intExtra(name: String): Int = intExtraOrNull(name) ?: missingExtraError(name)
+
+ fun Intent.stringExtraOrNull(name: String): String? = getStringExtra(name)
+
+ fun Intent.stringExtra(name: String): String = stringExtraOrNull(name) ?: missingExtraError(name)
+
+ fun Intent.booleanExtraOrNull(name: String): Boolean? =
+ if (hasExtra(name)) getBooleanExtra(name, false) else null
+
+ fun Intent.booleanExtra(name: String): Boolean =
+ booleanExtraOrNull(name) ?: missingExtraError(name)
+
+ fun Intent.bundleExtraOrNull(name: String): Bundle? = getBundleExtra(name)
+
+ fun Intent.bundleExtra(name: String): Bundle = bundleExtraOrNull(name) ?: missingExtraError(name)
+
+ fun missingExtraError(name: String): Nothing = error("Required extra '$name' is missing")
+
+ @RequiresApi(VERSION_CODES.TIRAMISU)
+ inline fun <reified T> Intent.parcelableExtraOrNull(name: String): T? =
+ getParcelableExtra(name, T::class.java)
+
+ @RequiresApi(VERSION_CODES.TIRAMISU)
+ inline fun <reified T> Intent.parcelableExtra(name: String): T =
+ parcelableExtraOrNull(name) ?: missingExtraError(name)
+
+ operator fun Intent.set(key: String, value: Boolean?) = apply { value?.let { putExtra(key, it) } }
+
+ operator fun Intent.set(key: String, value: Byte?) = apply { value?.let { putExtra(key, it) } }
+
+ operator fun Intent.set(key: String, value: Char?) = apply { value?.let { putExtra(key, it) } }
+
+ operator fun Intent.set(key: String, value: Short?) = apply { value?.let { putExtra(key, it) } }
+
+ operator fun Intent.set(key: String, value: Int?) = apply { value?.let { putExtra(key, it) } }
+
+ operator fun Intent.set(key: String, value: Long?) = apply { value?.let { putExtra(key, it) } }
+
+ operator fun Intent.set(key: String, value: Float?) = apply { value?.let { putExtra(key, it) } }
+
+ operator fun Intent.set(key: String, value: Double?) = apply { value?.let { putExtra(key, it) } }
+
+ operator fun Intent.set(key: String, value: String?) = putExtra(key, value)
+
+ operator fun Intent.set(key: String, value: CharSequence?) = putExtra(key, value)
+
+ operator fun Intent.set(key: String, value: Parcelable?) = putExtra(key, value)
+
+ operator fun Intent.set(key: String, value: Bundle?) = putExtra(key, value)
+
+ operator fun Intent.set(key: String, value: Serializable?) = putExtra(key, value)
+}
diff --git a/src/com/android/onboarding/common/OnboardingContext.kt b/src/com/android/onboarding/common/OnboardingContext.kt
new file mode 100644
index 0000000..c39f4b1
--- /dev/null
+++ b/src/com/android/onboarding/common/OnboardingContext.kt
@@ -0,0 +1,10 @@
+package com.android.onboarding.common
+
+import javax.inject.Qualifier
+import kotlin.annotation.Retention
+
+/**
+ * Annotation for requesting a dedicated Context for onboarding components. Most of the time this
+ * will be an ApplicationContext.
+ */
+@Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class OnboardingContext
diff --git a/src/com/android/onboarding/contracts/Android.bp b/src/com/android/onboarding/contracts/Android.bp
index 3003cb0..156426c 100644
--- a/src/com/android/onboarding/contracts/Android.bp
+++ b/src/com/android/onboarding/contracts/Android.bp
@@ -12,5 +12,6 @@ android_library {
static_libs: [
"androidx.activity_activity-ktx",
"android_onboarding.nodes",
+ "error_prone_annotations",
],
}
diff --git a/src/com/android/onboarding/contracts/ArgumentValidator.kt b/src/com/android/onboarding/contracts/ArgumentValidator.kt
new file mode 100644
index 0000000..382fb5f
--- /dev/null
+++ b/src/com/android/onboarding/contracts/ArgumentValidator.kt
@@ -0,0 +1,27 @@
+package com.android.onboarding.contracts
+
+import com.google.errorprone.annotations.CanIgnoreReturnValue
+import kotlin.jvm.Throws
+
+/** Implementing entities provide means to validate the correctness of [A] */
+interface ArgumentValidator<A : Any> {
+ /**
+ * Validates given object or throws an exception
+ *
+ * @receiver the object to validate
+ */
+ @Throws(IllegalArgumentException::class) fun A.validate()
+}
+
+/**
+ * Chaining-enabled validation condition that throws an error if it's not met
+ *
+ * @param message to pass when throwing an error
+ * @param condition lambda describing correct state
+ * @receiver the value to validate
+ */
+@Throws(IllegalArgumentException::class)
+@CanIgnoreReturnValue
+inline fun <V> V.require(message: String, condition: (V) -> Boolean): V = also {
+ require(condition(this)) { message }
+}
diff --git a/src/com/android/onboarding/contracts/ContractResult.kt b/src/com/android/onboarding/contracts/ContractResult.kt
index 836440e..5c4f74d 100644
--- a/src/com/android/onboarding/contracts/ContractResult.kt
+++ b/src/com/android/onboarding/contracts/ContractResult.kt
@@ -20,4 +20,9 @@ sealed interface ContractResult {
internal data class UnknownContractResult(
override val resultCode: Int,
override val intent: Intent? = null,
-) : ContractResult \ No newline at end of file
+) : ContractResult
+
+interface ContractResultSerializer<R> {
+ fun read(result: ContractResult): R
+ fun write(result: R): ContractResult
+}
diff --git a/src/com/android/onboarding/contracts/IntentSerializer.kt b/src/com/android/onboarding/contracts/IntentSerializer.kt
index e0a9daf..cc11190 100644
--- a/src/com/android/onboarding/contracts/IntentSerializer.kt
+++ b/src/com/android/onboarding/contracts/IntentSerializer.kt
@@ -1,6 +1,10 @@
package com.android.onboarding.contracts
import android.content.Intent
+import android.os.Build
+import android.os.Bundle
+import android.os.Parcelable
+import androidx.annotation.RequiresApi
/** Interface for writing an object to an [Intent] and reading the same object from an [Intent]. */
interface IntentSerializer<V> {
@@ -8,3 +12,138 @@ interface IntentSerializer<V> {
fun read(intent: Intent): V
}
+
+/**
+ * A serializer that does not expose [Intent] directly and instead works via [IntentScope]
+ * abstraction that can be observed
+ *
+ * TODO This is living as a separate opt-in interface for now, but
+ * we should look into incorporating it inside the contract class to make methods enforceable and
+ * protected
+ */
+interface ScopedIntentSerializer<V> : IntentSerializer<V> {
+ override fun write(intent: Intent, value: V) = IntentScope(intent).write(value)
+
+ override fun read(intent: Intent): V = IntentScope(intent).read()
+
+ fun IntentScope.write(value: V)
+
+ /**
+ * Writes a [value] to the [IntentScope] via a given [serializer]
+ */
+ fun <T> IntentScope.write(serializer: ScopedIntentSerializer<T>, value: T): Unit =
+ with(serializer) { write(value) }
+
+ fun IntentScope.read(): V
+
+ /**
+ * Reads a value [T] from the [IntentScope] via a given [serializer]
+ */
+ fun <T> IntentScope.read(serializer: ScopedIntentSerializer<T>): T = with(serializer) { read() }
+}
+
+/**
+ * An observable abstraction over [Intent] extras.
+ *
+ * @param intent to wrap
+ * @param afterRead function to call after each read
+ * @param beforeWrite function to call before each write
+ */
+data class IntentScope(
+ @PublishedApi internal val intent: Intent,
+ @PublishedApi internal val afterRead: (key: String, value: Any?) -> Unit = { _, _ -> },
+ @PublishedApi internal val beforeWrite: (key: String, value: Any?) -> Unit = { _, _ -> },
+) {
+ operator fun contains(key: String): Boolean = intent.hasExtra(key)
+
+ fun intExtraOrNull(key: String): Int? =
+ run { if (contains(key)) intent.getIntExtra(key, 0) else null }.also { afterRead(key, it) }
+
+ fun intExtra(key: String): Int = intExtraOrNull(key) ?: missingExtraError(key)
+
+ fun stringExtraOrNull(key: String): String? =
+ intent.getStringExtra(key).also { afterRead(key, it) }
+
+ fun stringExtra(key: String): String = stringExtraOrNull(key) ?: missingExtraError(key)
+
+ fun booleanExtraOrNull(key: String): Boolean? =
+ run { if (contains(key)) intent.getBooleanExtra(key, false) else null }
+ .also { afterRead(key, it) }
+
+ fun booleanExtra(key: String): Boolean = booleanExtraOrNull(key) ?: missingExtraError(key)
+
+ fun bundleExtraOrNull(key: String): Bundle? =
+ intent.getBundleExtra(key).also { afterRead(key, it) }
+
+ fun bundleExtra(key: String): Bundle = bundleExtraOrNull(key) ?: missingExtraError(key)
+
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ inline fun <reified T> parcelableExtraOrNull(key: String): T? =
+ intent.getParcelableExtra(key, T::class.java).also { afterRead(key, it) }
+
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ inline fun <reified T> parcelableExtra(key: String): T =
+ parcelableExtraOrNull(key) ?: missingExtraError(key)
+
+ fun missingExtraError(key: String): Nothing = error("Required extra '$key' is missing")
+
+ operator fun set(key: String, value: Boolean?): IntentScope = apply {
+ beforeWrite(key, value)
+ value?.let { intent.putExtra(key, it) }
+ }
+
+ operator fun set(key: String, value: Byte?): IntentScope = apply {
+ beforeWrite(key, value)
+ value?.let { intent.putExtra(key, it) }
+ }
+
+ operator fun set(key: String, value: Char?): IntentScope = apply {
+ beforeWrite(key, value)
+ value?.let { intent.putExtra(key, it) }
+ }
+
+ operator fun set(key: String, value: Short?): IntentScope = apply {
+ beforeWrite(key, value)
+ value?.let { intent.putExtra(key, it) }
+ }
+
+ operator fun set(key: String, value: Int?): IntentScope = apply {
+ beforeWrite(key, value)
+ value?.let { intent.putExtra(key, it) }
+ }
+
+ operator fun set(key: String, value: Long?): IntentScope = apply {
+ beforeWrite(key, value)
+ value?.let { intent.putExtra(key, it) }
+ }
+
+ operator fun set(key: String, value: Float?): IntentScope = apply {
+ beforeWrite(key, value)
+ value?.let { intent.putExtra(key, it) }
+ }
+
+ operator fun set(key: String, value: Double?): IntentScope = apply {
+ beforeWrite(key, value)
+ value?.let { intent.putExtra(key, it) }
+ }
+
+ operator fun set(key: String, value: String?): IntentScope = apply {
+ beforeWrite(key, value)
+ value?.let { intent.putExtra(key, value) }
+ }
+
+ operator fun set(key: String, value: CharSequence?): IntentScope = apply {
+ beforeWrite(key, value)
+ value?.let { intent.putExtra(key, value) }
+ }
+
+ operator fun set(key: String, value: Parcelable?): IntentScope = apply {
+ beforeWrite(key, value)
+ value?.let { intent.putExtra(key, value) }
+ }
+
+ operator fun set(key: String, value: Bundle?): IntentScope = apply {
+ beforeWrite(key, value)
+ value?.let { intent.putExtra(key, value) }
+ }
+}
diff --git a/src/com/android/onboarding/contracts/OnboardingActivityApiContract.kt b/src/com/android/onboarding/contracts/OnboardingActivityApiContract.kt
index 49eac22..37c14c4 100644
--- a/src/com/android/onboarding/contracts/OnboardingActivityApiContract.kt
+++ b/src/com/android/onboarding/contracts/OnboardingActivityApiContract.kt
@@ -91,13 +91,9 @@ abstract class OnboardingActivityApiContract<I, O> : ActivityResultContract<I, O
}
private fun intentToIntentData(intent: Intent): OnboardingGraphLog.OnboardingEvent.IntentData {
- val extras = mutableMapOf<String, Any?>()
-
- intent.extras?.let {
- for (s in it.keySet()) {
- extras[s] = it.get(s)
+ val extras = buildMap<String, Any?> {
+ intent.extras?.let { for (key in it.keySet()) put(key, it.get(key)) }
}
- }
return OnboardingGraphLog.OnboardingEvent.IntentData(intent.action, extras)
}
@@ -129,7 +125,7 @@ abstract class OnboardingActivityApiContract<I, O> : ActivityResultContract<I, O
*
* <p>When parsing fails, the failure will be recorded so that it can be fixed.
*/
- fun validate(activity: Activity, intent: Intent) {
+ fun validate(activity: Activity, intent: Intent = activity.intent) {
try {
AndroidOnboardingGraphLog.log(
ActivityNodeValidating(activity.nodeId, this.javaClass.name, intentToIntentData(intent))
diff --git a/src/com/android/onboarding/contracts/provisioning/ACTIONS.kt b/src/com/android/onboarding/contracts/provisioning/ACTIONS.kt
index 54f173e..8bac608 100644
--- a/src/com/android/onboarding/contracts/provisioning/ACTIONS.kt
+++ b/src/com/android/onboarding/contracts/provisioning/ACTIONS.kt
@@ -63,6 +63,14 @@ object ACTIONS {
inline val ACTION_PROVISION_MANAGED_DEVICE: String
get() = DevicePolicyManager.ACTION_PROVISION_MANAGED_DEVICE
+ /**
+ * Cannot link against it directly because it's marked as `@hidden` without `@SystemApi`
+ *
+ * @see DevicePolicyManager.ACTION_PROVISION_MANAGED_USER
+ */
+ inline val ACTION_PROVISION_MANAGED_USER: String
+ get() = "android.app.action.PROVISION_MANAGED_USER"
+
inline val ACTION_PROVISION_MANAGED_PROFILE: String
get() = DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE
diff --git a/src/com/android/onboarding/contracts/provisioning/Android.bp b/src/com/android/onboarding/contracts/provisioning/Android.bp
index 6b3d6b8..7b9d1fc 100644
--- a/src/com/android/onboarding/contracts/provisioning/Android.bp
+++ b/src/com/android/onboarding/contracts/provisioning/Android.bp
@@ -10,10 +10,13 @@ android_library {
],
dont_merge_manifests: true,
static_libs: [
+ "android_onboarding.common",
"android_onboarding.contracts",
+ "android_onboarding.contracts.setupwizard",
"android_onboarding.contracts.annotations",
"setupcompat",
"androidx.annotation_annotation",
+ "jsr330"
],
platform_apis: true,
}
diff --git a/src/com/android/onboarding/contracts/provisioning/ArgumentSerializer.kt b/src/com/android/onboarding/contracts/provisioning/ArgumentSerializer.kt
deleted file mode 100644
index 08672f9..0000000
--- a/src/com/android/onboarding/contracts/provisioning/ArgumentSerializer.kt
+++ /dev/null
@@ -1,70 +0,0 @@
-package com.android.onboarding.contracts.provisioning
-
-import android.content.Intent
-import android.os.Build.VERSION_CODES
-import android.os.Bundle
-import android.os.Parcelable
-import androidx.annotation.RequiresApi
-import java.io.Serializable
-
-/** Utility parent class providing scoped DSL utilities for concise intent extra manipulation */
-@Suppress("MemberVisibilityCanBePrivate")
-abstract class ArgumentSerializer {
- protected fun Intent.intExtraOrNull(name: String): Int? =
- if (hasExtra(name)) getIntExtra(name, 0) else null
-
- protected fun Intent.intExtra(name: String): Int = intExtraOrNull(name) ?: missingExtraError(name)
-
- protected fun Intent.stringExtraOrNull(name: String): String? = getStringExtra(name)
-
- protected fun Intent.stringExtra(name: String): String =
- stringExtraOrNull(name) ?: missingExtraError(name)
-
- protected fun Intent.booleanExtraOrNull(name: String): Boolean? =
- if (hasExtra(name)) getBooleanExtra(name, false) else null
-
- protected fun Intent.booleanExtra(name: String): Boolean =
- booleanExtraOrNull(name) ?: missingExtraError(name)
-
- protected fun Intent.bundleExtraOrNull(name: String): Bundle? = getBundleExtra(name)
-
- protected fun Intent.bundleExtra(name: String): Bundle =
- bundleExtraOrNull(name) ?: missingExtraError(name)
-
- protected fun missingExtraError(name: String): Nothing =
- error("Required extra '$name' is missing")
-
- @RequiresApi(VERSION_CODES.UPSIDE_DOWN_CAKE)
- protected inline fun <reified T> Intent.parcelableExtraOrNull(name: String): T? =
- getParcelableExtra(name, T::class.java)
-
- @RequiresApi(VERSION_CODES.UPSIDE_DOWN_CAKE)
- protected inline fun <reified T> Intent.parcelableExtra(name: String): T =
- parcelableExtraOrNull(name) ?: missingExtraError(name)
-
- protected operator fun Intent.set(key: String, value: Boolean) = putExtra(key, value)
-
- protected operator fun Intent.set(key: String, value: Byte) = putExtra(key, value)
-
- protected operator fun Intent.set(key: String, value: Char) = putExtra(key, value)
-
- protected operator fun Intent.set(key: String, value: Short) = putExtra(key, value)
-
- protected operator fun Intent.set(key: String, value: Int) = putExtra(key, value)
-
- protected operator fun Intent.set(key: String, value: Long) = putExtra(key, value)
-
- protected operator fun Intent.set(key: String, value: Float) = putExtra(key, value)
-
- protected operator fun Intent.set(key: String, value: Double) = putExtra(key, value)
-
- protected operator fun Intent.set(key: String, value: String?) = putExtra(key, value)
-
- protected operator fun Intent.set(key: String, value: CharSequence?) = putExtra(key, value)
-
- protected operator fun Intent.set(key: String, value: Parcelable?) = putExtra(key, value)
-
- protected operator fun Intent.set(key: String, value: Bundle?) = putExtra(key, value)
-
- protected operator fun Intent.set(key: String, value: Serializable?) = putExtra(key, value)
-}
diff --git a/src/com/android/onboarding/contracts/provisioning/EXTRAS.kt b/src/com/android/onboarding/contracts/provisioning/EXTRAS.kt
index cf1edca..2609a00 100644
--- a/src/com/android/onboarding/contracts/provisioning/EXTRAS.kt
+++ b/src/com/android/onboarding/contracts/provisioning/EXTRAS.kt
@@ -4,6 +4,10 @@ import android.app.admin.DevicePolicyManager
import com.google.android.setupcompat.util.WizardManagerHelper
object EXTRAS {
+ /** @see com.android.managedprovisioning.model.ProvisioningParams.EXTRA_PROVISIONING_PARAMS */
+ inline val EXTRA_PROVISIONING_PARAMS: String
+ get() = "provisioningParams"
+
inline val EXTRA_PROVISIONING_ADMIN_EXTRAS_BUNDLE: String
get() = DevicePolicyManager.EXTRA_PROVISIONING_ADMIN_EXTRAS_BUNDLE
diff --git a/src/com/android/onboarding/contracts/provisioning/FLAGS.kt b/src/com/android/onboarding/contracts/provisioning/FLAGS.kt
new file mode 100644
index 0000000..24d0a46
--- /dev/null
+++ b/src/com/android/onboarding/contracts/provisioning/FLAGS.kt
@@ -0,0 +1,11 @@
+package com.android.onboarding.contracts.provisioning
+
+import android.app.admin.DevicePolicyManager
+
+object FLAGS {
+ inline val FLAG_SUPPORTED_MODES_ORGANIZATION_OWNED: Int
+ get() = DevicePolicyManager.FLAG_SUPPORTED_MODES_ORGANIZATION_OWNED
+
+ inline val FLAG_SUPPORTED_MODES_DEVICE_OWNER: Int
+ get() = DevicePolicyManager.FLAG_SUPPORTED_MODES_DEVICE_OWNER
+}
diff --git a/src/com/android/onboarding/contracts/provisioning/FinalizationInsideSuwContract.kt b/src/com/android/onboarding/contracts/provisioning/FinalizationInsideSuwContract.kt
new file mode 100644
index 0000000..73f594a
--- /dev/null
+++ b/src/com/android/onboarding/contracts/provisioning/FinalizationInsideSuwContract.kt
@@ -0,0 +1,45 @@
+package com.android.onboarding.contracts.provisioning
+
+import android.content.Context
+import android.content.Intent
+import com.android.onboarding.contracts.ContractResult
+import com.android.onboarding.contracts.IntentScope
+import com.android.onboarding.contracts.OnboardingActivityApiContract
+import com.android.onboarding.contracts.ScopedIntentSerializer
+import com.android.onboarding.contracts.setupwizard.SuwArguments
+import com.android.onboarding.contracts.setupwizard.SuwArgumentsSerializer
+import com.android.onboarding.contracts.setupwizard.WithOptionalSuwArguments
+import javax.inject.Inject
+
+data class FinalizationInsideSuwArguments(
+ override val suwArguments: SuwArguments?,
+) : WithOptionalSuwArguments
+
+/**
+ * Result is propagated from the child activities the activity described by this contract launches.
+ *
+ * The activity uses [ProvisioningParams], however they are fetched from the FS rather than intent extras.
+ */
+class FinalizationInsideSuwContract
+@Inject
+constructor(val suwArgumentsSerializer: SuwArgumentsSerializer) :
+ OnboardingActivityApiContract<FinalizationInsideSuwArguments, ContractResult>(),
+ ScopedIntentSerializer<FinalizationInsideSuwArguments> {
+ override fun performCreateIntent(context: Context, arg: FinalizationInsideSuwArguments): Intent =
+ Intent(ACTIONS.ACTION_ROLE_HOLDER_PROVISION_FINALIZATION).also { write(it, arg) }
+
+ override fun IntentScope.write(value: FinalizationInsideSuwArguments) {
+ value.suwArguments?.let { write(suwArgumentsSerializer, it) }
+ }
+
+ override fun IntentScope.read(): FinalizationInsideSuwArguments =
+ FinalizationInsideSuwArguments(
+ suwArguments = read(suwArgumentsSerializer),
+ )
+
+ override fun performExtractArgument(intent: Intent): FinalizationInsideSuwArguments = read(intent)
+
+ override fun performParseResult(result: ContractResult): ContractResult = result
+
+ override fun performSetResult(result: ContractResult): ContractResult = result
+}
diff --git a/src/com/android/onboarding/contracts/provisioning/managed/profile/Android.bp b/src/com/android/onboarding/contracts/provisioning/managed/profile/Android.bp
index 32e7924..9e3e4f0 100644
--- a/src/com/android/onboarding/contracts/provisioning/managed/profile/Android.bp
+++ b/src/com/android/onboarding/contracts/provisioning/managed/profile/Android.bp
@@ -10,8 +10,9 @@ android_library {
],
dont_merge_manifests: true,
static_libs: [
+ "android_onboarding.common",
"android_onboarding.contracts",
"android_onboarding.contracts.provisioning",
- "android_onboarding.contracts.provisioning.suw",
+ "android_onboarding.contracts.setupwizard",
],
}
diff --git a/src/com/android/onboarding/contracts/provisioning/managed/profile/ProvisionManagedProfileArguments.kt b/src/com/android/onboarding/contracts/provisioning/managed/profile/ProvisionManagedProfileArguments.kt
index fc1ce15..c15240e 100644
--- a/src/com/android/onboarding/contracts/provisioning/managed/profile/ProvisionManagedProfileArguments.kt
+++ b/src/com/android/onboarding/contracts/provisioning/managed/profile/ProvisionManagedProfileArguments.kt
@@ -2,12 +2,14 @@ package com.android.onboarding.contracts.provisioning.managed.profile
import android.content.pm.ActivityInfo
import android.net.Uri
-import com.android.onboarding.contracts.provisioning.suw.SuwArguments
+import com.android.onboarding.contracts.setupwizard.SuwArguments
+import com.android.onboarding.contracts.setupwizard.WithSuwArguments
/** */
-interface ProvisionManagedProfileArguments : SuwArguments {
- val trigger: Int
- val deviceAdminComponentName: ActivityInfo
- val deviceAdminSignatureChecksum: String
- val deviceAdminPackageDownloadLocation: Uri
-}
+data class ProvisionManagedProfileArguments(
+ override val suwArguments: SuwArguments,
+ val trigger: Int,
+ val deviceAdminComponentName: ActivityInfo,
+ val deviceAdminSignatureChecksum: String,
+ val deviceAdminPackageDownloadLocation: Uri,
+) : WithSuwArguments
diff --git a/src/com/android/onboarding/contracts/provisioning/managed/profile/ProvisionManagedProfileArgumentsSerializer.kt b/src/com/android/onboarding/contracts/provisioning/managed/profile/ProvisionManagedProfileArgumentsSerializer.kt
index c4d4358..a88d171 100644
--- a/src/com/android/onboarding/contracts/provisioning/managed/profile/ProvisionManagedProfileArgumentsSerializer.kt
+++ b/src/com/android/onboarding/contracts/provisioning/managed/profile/ProvisionManagedProfileArgumentsSerializer.kt
@@ -1,45 +1,40 @@
package com.android.onboarding.contracts.provisioning.managed.profile
import android.content.Intent
-import android.content.pm.ActivityInfo
import android.net.Uri
import com.android.onboarding.contracts.IntentSerializer
-import com.android.onboarding.contracts.provisioning.ArgumentSerializer
import com.android.onboarding.contracts.provisioning.EXTRAS
-import com.android.onboarding.contracts.provisioning.suw.SuwArguments
-import com.android.onboarding.contracts.provisioning.suw.SuwArgumentsSerializer
+import com.android.onboarding.common.IntentWriter
+import com.android.onboarding.contracts.setupwizard.SuwArgumentsSerializer
class ProvisionManagedProfileArgumentsSerializer(
private val suwArgumentsSerializer: SuwArgumentsSerializer = SuwArgumentsSerializer(),
-) : ArgumentSerializer(), IntentSerializer<ProvisionManagedProfileArguments> {
+) : IntentSerializer<ProvisionManagedProfileArguments> {
override fun write(intent: Intent, value: ProvisionManagedProfileArguments): Unit =
- with(value) {
- suwArgumentsSerializer.write(intent, value)
- intent["trigger"] = trigger
- intent[EXTRAS.EXTRA_PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME] = deviceAdminComponentName
+ with(IntentWriter) {
+ suwArgumentsSerializer.write(intent, value.suwArguments)
+ intent["trigger"] = value.trigger
+ intent[EXTRAS.EXTRA_PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME] = value.deviceAdminComponentName
intent[EXTRAS.EXTRA_PROVISIONING_DEVICE_ADMIN_SIGNATURE_CHECKSUM] =
- deviceAdminSignatureChecksum
+ value.deviceAdminSignatureChecksum
intent[EXTRAS.EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_DOWNLOAD_LOCATION] =
- deviceAdminPackageDownloadLocation
+ value.deviceAdminPackageDownloadLocation
}
- override fun read(intent: Intent): ProvisionManagedProfileArguments {
- return object :
- ProvisionManagedProfileArguments, SuwArguments by suwArgumentsSerializer.read(intent) {
-
- override val trigger: Int = intent.intExtra(EXTRAS.EXTRA_PROVISIONING_TRIGGER)
-
- override val deviceAdminComponentName: ActivityInfo =
- intent.parcelableExtra(EXTRAS.EXTRA_PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME)
-
- override val deviceAdminSignatureChecksum: String =
- intent.stringExtra(EXTRAS.EXTRA_PROVISIONING_DEVICE_ADMIN_SIGNATURE_CHECKSUM)
-
- override val deviceAdminPackageDownloadLocation: Uri =
- intent
- .stringExtra(EXTRAS.EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_DOWNLOAD_LOCATION)
- .let(Uri::parse)
+ override fun read(intent: Intent): ProvisionManagedProfileArguments =
+ with(IntentWriter) {
+ ProvisionManagedProfileArguments(
+ suwArguments = suwArgumentsSerializer.read(intent),
+ trigger = intent.intExtra(EXTRAS.EXTRA_PROVISIONING_TRIGGER),
+ deviceAdminComponentName =
+ intent.parcelableExtra(EXTRAS.EXTRA_PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME),
+ deviceAdminSignatureChecksum =
+ intent.stringExtra(EXTRAS.EXTRA_PROVISIONING_DEVICE_ADMIN_SIGNATURE_CHECKSUM),
+ deviceAdminPackageDownloadLocation =
+ intent
+ .stringExtra(EXTRAS.EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_DOWNLOAD_LOCATION)
+ .let(Uri::parse),
+ )
}
- }
}
diff --git a/src/com/android/onboarding/contracts/provisioning/managed/profile/ProvisionManagedProfileContract.kt b/src/com/android/onboarding/contracts/provisioning/managed/profile/ProvisionManagedProfileContract.kt
index f24c203..e14ae85 100644
--- a/src/com/android/onboarding/contracts/provisioning/managed/profile/ProvisionManagedProfileContract.kt
+++ b/src/com/android/onboarding/contracts/provisioning/managed/profile/ProvisionManagedProfileContract.kt
@@ -10,9 +10,9 @@ import com.android.onboarding.contracts.provisioning.ACTIONS
* |----------|-------------------------------------------------------------------------|
* | | |
*/
-class ProvisionManagedProfileContract :
- VoidOnboardingActivityApiContract<ProvisionManagedProfileArguments>() {
- private val serializer = ProvisionManagedProfileArgumentsSerializer()
+class ProvisionManagedProfileContract(
+ private val serializer: ProvisionManagedProfileArgumentsSerializer = ProvisionManagedProfileArgumentsSerializer(),
+) : VoidOnboardingActivityApiContract<ProvisionManagedProfileArguments>() {
override fun performCreateIntent(
context: Context,
diff --git a/src/com/android/onboarding/contracts/provisioning/suw/SuwArguments.kt b/src/com/android/onboarding/contracts/provisioning/suw/SuwArguments.kt
deleted file mode 100644
index ffd1e60..0000000
--- a/src/com/android/onboarding/contracts/provisioning/suw/SuwArguments.kt
+++ /dev/null
@@ -1,22 +0,0 @@
-package com.android.onboarding.contracts.provisioning.suw
-
-import android.net.Uri
-import android.os.Bundle
-
-/**
- * A set of common arguments unused by provisioning but still passed around to other processes.
- */
-interface SuwArguments {
- val actionId: Int?
- val isSubactivityFirstLaunched: Boolean
- val isSuwSuggestedActionFlow: Boolean
- val isSetupFlow: Boolean
- val preDeferredSetup: Boolean
- val deferredSetup: Boolean
- val firstRun: Boolean
- val portalSetup: Boolean
- val scriptUri: Uri?
- val hasMultipleUsers: Boolean
- val theme: String
- val wizardBundle: Bundle
-}
diff --git a/src/com/android/onboarding/contracts/provisioning/suw/SuwArgumentsSerializer.kt b/src/com/android/onboarding/contracts/provisioning/suw/SuwArgumentsSerializer.kt
deleted file mode 100644
index 6f6611e..0000000
--- a/src/com/android/onboarding/contracts/provisioning/suw/SuwArgumentsSerializer.kt
+++ /dev/null
@@ -1,56 +0,0 @@
-package com.android.onboarding.contracts.provisioning.suw
-
-import android.content.Intent
-import android.net.Uri
-import android.os.Bundle
-import com.android.onboarding.contracts.IntentSerializer
-import com.android.onboarding.contracts.provisioning.ArgumentSerializer
-
-class SuwArgumentsSerializer : ArgumentSerializer(), IntentSerializer<SuwArguments> {
-
- override fun write(intent: Intent, value: SuwArguments): Unit =
- with(value) {
- intent["actionId"] = actionId
- intent["isSubactivityFirstLaunched"] = isSubactivityFirstLaunched
- intent["isSuwSuggestedActionFlow"] = isSuwSuggestedActionFlow
- intent["isSetupFlow"] = isSetupFlow
- intent["preDeferredSetup"] = preDeferredSetup
- intent["deferredSetup"] = deferredSetup
- intent["firstRun"] = firstRun
- intent["portalSetup"] = portalSetup
- intent["scriptUri"] = scriptUri
- intent["hasMultipleUsers"] = hasMultipleUsers
- intent["theme"] = theme
- intent["wizardBundle"] = wizardBundle
- }
-
- override fun read(intent: Intent): SuwArguments {
- return object : SuwArguments {
- override val actionId: Int? = intent.intExtraOrNull("actionId")
-
- override val isSubactivityFirstLaunched: Boolean =
- intent.booleanExtra("isSubactivityFirstLaunched")
-
- override val isSuwSuggestedActionFlow: Boolean =
- intent.booleanExtra("isSuwSuggestedActionFlow")
-
- override val isSetupFlow: Boolean = intent.booleanExtra("isSetupFlow")
-
- override val preDeferredSetup: Boolean = intent.booleanExtra("preDeferredSetup")
-
- override val deferredSetup: Boolean = intent.booleanExtra("deferredSetup")
-
- override val firstRun: Boolean = intent.booleanExtra("firstRun")
-
- override val portalSetup: Boolean = intent.booleanExtra("portalSetup")
-
- override val scriptUri: Uri? = intent.parcelableExtra("scriptUri")
-
- override val hasMultipleUsers: Boolean = intent.booleanExtra("hasMultipleUsers")
-
- override val theme: String = intent.stringExtra("theme")
-
- override val wizardBundle: Bundle = intent.bundleExtra("wizardBundle")
- }
- }
-}
diff --git a/src/com/android/onboarding/contracts/provisioning/suw/Android.bp b/src/com/android/onboarding/contracts/setupwizard/Android.bp
index 25a77c1..e275000 100644
--- a/src/com/android/onboarding/contracts/provisioning/suw/Android.bp
+++ b/src/com/android/onboarding/contracts/setupwizard/Android.bp
@@ -3,14 +3,16 @@ package {
}
android_library {
- name: "android_onboarding.contracts.provisioning.suw",
+ name: "android_onboarding.contracts.setupwizard",
manifest: ":android_onboarding.AndroidManifest",
srcs: [
"*.kt",
],
dont_merge_manifests: true,
static_libs: [
+ "android_onboarding.common",
"android_onboarding.contracts",
- "android_onboarding.contracts.provisioning",
+ "setupcompat",
+ "jsr330"
],
}
diff --git a/src/com/android/onboarding/contracts/setupwizard/SuwArguments.kt b/src/com/android/onboarding/contracts/setupwizard/SuwArguments.kt
new file mode 100644
index 0000000..8b5b032
--- /dev/null
+++ b/src/com/android/onboarding/contracts/setupwizard/SuwArguments.kt
@@ -0,0 +1,36 @@
+package com.android.onboarding.contracts.setupwizard
+
+import android.net.Uri
+import android.os.Bundle
+
+/**
+ * Marker interface to keep track of all our contract arguments that either directly or indirectly
+ * depend on [SuwArguments]
+ */
+interface WithSuwArguments : WithOptionalSuwArguments {
+ override val suwArguments: SuwArguments
+}
+
+/**
+ * Marker interface to keep track of all our contract arguments that optionally either directly or
+ * indirectly depend on [SuwArguments]
+ */
+interface WithOptionalSuwArguments {
+ val suwArguments: SuwArguments?
+}
+
+/** A set of common arguments unused by provisioning but still passed around to other processes. */
+data class SuwArguments(
+ val actionId: Int?,
+ val isSubactivityFirstLaunched: Boolean?,
+ val isSuwSuggestedActionFlow: Boolean,
+ val isSetupFlow: Boolean,
+ val preDeferredSetup: Boolean,
+ val deferredSetup: Boolean,
+ val firstRun: Boolean,
+ val portalSetup: Boolean,
+ val scriptUri: Uri?,
+ val hasMultipleUsers: Boolean,
+ val theme: String,
+ val wizardBundle: Bundle,
+)
diff --git a/src/com/android/onboarding/contracts/setupwizard/SuwArgumentsSerializer.kt b/src/com/android/onboarding/contracts/setupwizard/SuwArgumentsSerializer.kt
new file mode 100644
index 0000000..bee411d
--- /dev/null
+++ b/src/com/android/onboarding/contracts/setupwizard/SuwArgumentsSerializer.kt
@@ -0,0 +1,39 @@
+package com.android.onboarding.contracts.setupwizard
+
+import com.android.onboarding.contracts.IntentScope
+import com.android.onboarding.contracts.ScopedIntentSerializer
+import com.google.android.setupcompat.util.WizardManagerHelper
+import javax.inject.Inject
+
+class SuwArgumentsSerializer @Inject constructor() : ScopedIntentSerializer<SuwArguments> {
+ override fun IntentScope.write(value: SuwArguments) {
+ this["actionId"] = value.actionId
+ this["isSubactivityFirstLaunched"] = value.isSubactivityFirstLaunched
+ this[WizardManagerHelper.EXTRA_IS_SUW_SUGGESTED_ACTION_FLOW] = value.isSuwSuggestedActionFlow
+ this[WizardManagerHelper.EXTRA_IS_SETUP_FLOW] = value.isSetupFlow
+ this[WizardManagerHelper.EXTRA_IS_PRE_DEFERRED_SETUP] = value.preDeferredSetup
+ this[WizardManagerHelper.EXTRA_IS_DEFERRED_SETUP] = value.deferredSetup
+ this[WizardManagerHelper.EXTRA_IS_FIRST_RUN] = value.firstRun
+ this[WizardManagerHelper.EXTRA_IS_PORTAL_SETUP] = value.portalSetup
+ this["scriptUri"] = value.scriptUri
+ this["hasMultipleUsers"] = value.hasMultipleUsers
+ this[WizardManagerHelper.EXTRA_THEME] = value.theme
+ this["wizardBundle"] = value.wizardBundle
+ }
+
+ override fun IntentScope.read(): SuwArguments =
+ SuwArguments(
+ actionId = intExtraOrNull("actionId"),
+ isSubactivityFirstLaunched = booleanExtraOrNull("isSubactivityFirstLaunched"),
+ isSuwSuggestedActionFlow = booleanExtra("isSuwSuggestedActionFlow"),
+ isSetupFlow = booleanExtra("isSetupFlow"),
+ preDeferredSetup = booleanExtra("preDeferredSetup"),
+ deferredSetup = booleanExtra("deferredSetup"),
+ firstRun = booleanExtra("firstRun"),
+ portalSetup = booleanExtra("portalSetup"),
+ scriptUri = parcelableExtraOrNull("scriptUri"),
+ hasMultipleUsers = booleanExtra("hasMultipleUsers"),
+ theme = stringExtra("theme"),
+ wizardBundle = bundleExtra("wizardBundle"),
+ )
+}
diff --git a/src/com/android/onboarding/flags/Android.bp b/src/com/android/onboarding/flags/Android.bp
index 77402c0..ba4046a 100644
--- a/src/com/android/onboarding/flags/Android.bp
+++ b/src/com/android/onboarding/flags/Android.bp
@@ -11,5 +11,8 @@ android_library {
],
dont_merge_manifests: true,
static_libs: [
+ "jsr330",
+ "dagger2",
+ "android_onboarding.common"
],
}
diff --git a/src/com/android/onboarding/flags/DefaultOnboardingFlagsProvider.kt b/src/com/android/onboarding/flags/DefaultOnboardingFlagsProvider.kt
index 8515c79..9625f23 100644
--- a/src/com/android/onboarding/flags/DefaultOnboardingFlagsProvider.kt
+++ b/src/com/android/onboarding/flags/DefaultOnboardingFlagsProvider.kt
@@ -1,38 +1,32 @@
package com.android.onboarding.flags
-import android.content.ContentResolver
import android.content.Context
-import android.net.Uri
-import android.os.Bundle
+import android.os.SystemProperties
+import com.android.onboarding.common.OnboardingContext
+import javax.inject.Inject
-/** A default implementation of [OnboardingFlagsProvider]. */
-class DefaultOnboardingFlagsProvider(val appContext: Context) : OnboardingFlagsProvider {
+/** A default implementation of [OnboardingFlagsProvider] using system property. */
+class DefaultOnboardingFlagsProvider
+@Inject
+constructor(@OnboardingContext val appContext: Context) : OnboardingFlagsProvider {
- /**
- * [OnboardingFlagsProvider] access a specific content provider in the setup wizard, and setup
- * wizard maintains the onboarding contract flag logic behind itself.
- */
override fun isOnboardingContractEnabled(): Boolean {
- val requestFlag = arrayOf(FEATURE_ONBOARDING_CONTRACT_ENABLED)
- val requestBundle = Bundle().apply { putStringArray(EXTRA_KEY_FEATURES, requestFlag) }
-
- val result =
- appContext.contentResolver.call(
- getContentUri(),
- METHOD_GET_FEATURES,
- /* arg= */ null,
- /* extras= */ requestBundle
- )
- return result?.getBoolean(FEATURE_ONBOARDING_CONTRACT_ENABLED) ?: false
+ return SystemProperties.getBoolean(
+ SYSTEM_PROPERTY_NAMESPACE + FEATURE_ONBOARDING_CONTRACT_ENABLED,
+ false
+ )
}
- private fun getContentUri(): Uri =
- Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(SUW_AUTHORITY).build()
+ override fun isOnboardingNodeLoggingEnabled(): Boolean {
+ return SystemProperties.getBoolean(
+ SYSTEM_PROPERTY_NAMESPACE + FEATURE_ONBOARDING_NODE_LOGGING_ENABLED,
+ false
+ )
+ }
companion object {
- const val SUW_AUTHORITY = "com.google.android.setupwizard.FEATURES"
- const val METHOD_GET_FEATURES = "features"
- const val EXTRA_KEY_FEATURES = "features"
- const val FEATURE_ONBOARDING_CONTRACT_ENABLED = "is_onboarding_contract_enabled"
+ public const val SYSTEM_PROPERTY_NAMESPACE = "aoj.architecture.feature."
+ private const val FEATURE_ONBOARDING_CONTRACT_ENABLED = "is_onboarding_contract_enabled"
+ private const val FEATURE_ONBOARDING_NODE_LOGGING_ENABLED = "is_onboarding_node_logging_enabled"
}
}
diff --git a/src/com/android/onboarding/flags/OnboardingFlagsModule.kt b/src/com/android/onboarding/flags/OnboardingFlagsModule.kt
new file mode 100644
index 0000000..830b6a5
--- /dev/null
+++ b/src/com/android/onboarding/flags/OnboardingFlagsModule.kt
@@ -0,0 +1,12 @@
+package com.android.onboarding.flags
+
+import dagger.Binds
+import dagger.Module
+import dagger.Reusable
+
+@Module
+interface OnboardingFlagsModule {
+ @Binds
+ @Reusable
+ fun provideOnboardingFlagsProvider(impl: DefaultOnboardingFlagsProvider): OnboardingFlagsProvider
+}
diff --git a/src/com/android/onboarding/flags/OnboardingFlagsProvider.kt b/src/com/android/onboarding/flags/OnboardingFlagsProvider.kt
index a22ca4d..2d37786 100644
--- a/src/com/android/onboarding/flags/OnboardingFlagsProvider.kt
+++ b/src/com/android/onboarding/flags/OnboardingFlagsProvider.kt
@@ -4,8 +4,11 @@ package com.android.onboarding.flags
interface OnboardingFlagsProvider {
/**
- * True if onboarding contracts[setupwizard.feature.is_onboarding_contract_enabled] are being used
- * across the onboarding flow.
+ * Returns [true] if the onboarding contract architecture is being used across the onboarding
+ * flow.
*/
fun isOnboardingContractEnabled(): Boolean
+
+ /** Returns [true] if onboarding node logs should be uploaded remotely. */
+ fun isOnboardingNodeLoggingEnabled(): Boolean
}
diff --git a/src/com/android/onboarding/flags/testing/FakeOnboardingFlagsProvider.kt b/src/com/android/onboarding/flags/testing/FakeOnboardingFlagsProvider.kt
index 133b801..dfa7115 100644
--- a/src/com/android/onboarding/flags/testing/FakeOnboardingFlagsProvider.kt
+++ b/src/com/android/onboarding/flags/testing/FakeOnboardingFlagsProvider.kt
@@ -3,7 +3,11 @@ package com.android.onboarding.flags.testing
import com.android.onboarding.flags.OnboardingFlagsProvider
/** A fake implementation of [OnboardingFlagsProvider]. */
-class FakeOnboardingFlagsProvider(var isOnboardingContractEnabledFlag: Boolean = false) :
- OnboardingFlagsProvider {
+class FakeOnboardingFlagsProvider(
+ var isOnboardingContractEnabledFlag: Boolean = false,
+ var isOnboardingNodeLoggingEnabledFlag: Boolean = false
+) : OnboardingFlagsProvider {
override fun isOnboardingContractEnabled() = isOnboardingContractEnabledFlag
+
+ override fun isOnboardingNodeLoggingEnabled() = isOnboardingNodeLoggingEnabledFlag
}
diff --git a/src/com/android/onboarding/nodes/OnboardingGraph.kt b/src/com/android/onboarding/nodes/OnboardingGraph.kt
index 66a1cf3..1d878d1 100644
--- a/src/com/android/onboarding/nodes/OnboardingGraph.kt
+++ b/src/com/android/onboarding/nodes/OnboardingGraph.kt
@@ -138,7 +138,9 @@ class OnboardingGraph(events: Set<OnboardingGraphLog.OnboardingEvent>) {
if (e is ActivityNodeResultReceived) {
nodeMap.updateNode(e, e.nodeId, e.timestamp, e.nodeName, result = e.result)
val node = nodeMap[e.nodeId]!!
- nodeMap.updateNode(id = node._incomingEdge!!.id, timestamp = e.timestamp)
+ node._incomingEdge?.let {
+ nodeMap.updateNode(id = it.id, timestamp = e.timestamp)
+ }
} else if (e is ActivityNodeFail) {
nodeMap.updateNode(
e,
@@ -289,6 +291,9 @@ class OnboardingGraph(events: Set<OnboardingGraphLog.OnboardingEvent>) {
val isSynchronous: Boolean
get() = type == NodeType.SYNCHRONOUS
+ val isComplete: Boolean
+ get() = _events.size >= 2
+
override fun hashCode(): Int = Objects.hash(_id, _name)
override fun toString(): String =
diff --git a/src/com/android/onboarding/nodes/decoder/Base64ToProtoString.kt b/src/com/android/onboarding/nodes/decoder/Base64ToProtoString.kt
new file mode 100644
index 0000000..ca12f91
--- /dev/null
+++ b/src/com/android/onboarding/nodes/decoder/Base64ToProtoString.kt
@@ -0,0 +1,19 @@
+package com.android.onboarding.nodes.decoder
+
+import com.android.onboarding.nodes.OnboardingGraphLog
+
+/** Decode base64 encoded onboarding event proto to a string. */
+class Base64ToProtoString(val str: String) {
+
+ fun run() {
+ println(OnboardingGraphLog.OnboardingEvent.deserialize(str))
+ }
+
+ companion object {
+ @JvmStatic
+ fun main(args: Array<String>) {
+ if (args.isEmpty()) return
+ Base64ToProtoString(args[0]).run()
+ }
+ }
+}
diff --git a/src/com/android/onboarding/nodes/testing/testapp/AndroidManifest.xml b/src/com/android/onboarding/nodes/testing/testapp/AndroidManifest.xml
new file mode 100644
index 0000000..1f45db1
--- /dev/null
+++ b/src/com/android/onboarding/nodes/testing/testapp/AndroidManifest.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<manifest
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.onboarding.nodes.testing.testapp">
+
+ <uses-sdk
+ android:minSdkVersion="21"
+ android:targetSdkVersion="33"/>
+
+ <application
+ android:label="Onboarding Graph TestApp" android:theme="@style/Theme.AppCompat.Light" android:taskAffinity="">
+ <activity android:name=".MainActivity" android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.LAUNCHER"/>
+ <category android:name="android.intent.category.DEFAULT"/>
+ </intent-filter>
+ </activity>
+
+ <activity-alias
+ android:name=".RedActivity"
+ android:targetActivity=".MainActivity" android:exported="true">
+ <intent-filter>
+ <action android:name="com.android.onboarding.nodes.testing.testapp.red"/>
+ <category android:name="android.intent.category.DEFAULT"/>
+ </intent-filter>
+ </activity-alias>
+
+ <activity-alias
+ android:name=".GreenActivity"
+ android:targetActivity=".MainActivity" android:exported="true">
+ <intent-filter>
+ <action android:name="com.android.onboarding.nodes.testing.testapp.green"/>
+ <category android:name="android.intent.category.DEFAULT"/>
+ </intent-filter>
+ </activity-alias>
+
+ <activity-alias
+ android:name=".BlueActivity"
+ android:targetActivity=".MainActivity" android:exported="true">
+ <intent-filter>
+ <action android:name="com.android.onboarding.nodes.testing.testapp.blue"/>
+ <category android:name="android.intent.category.DEFAULT"/>
+ </intent-filter>
+ </activity-alias>
+ </application>
+</manifest> \ No newline at end of file
diff --git a/src/com/android/onboarding/nodes/testing/testapp/MainActivity.kt b/src/com/android/onboarding/nodes/testing/testapp/MainActivity.kt
new file mode 100644
index 0000000..55a3739
--- /dev/null
+++ b/src/com/android/onboarding/nodes/testing/testapp/MainActivity.kt
@@ -0,0 +1,103 @@
+package com.android.onboarding.nodes.testing.testapp
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.graphics.Color
+import android.os.Bundle
+import android.os.PersistableBundle
+import android.support.v7.app.AppCompatActivity
+import android.util.Log
+import android.widget.Button
+import android.widget.LinearLayout
+import com.android.onboarding.contracts.ContractResult
+import com.android.onboarding.contracts.OnboardingActivityApiContract
+
+class MainActivity : AppCompatActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ setContentView(R.layout.activity_main)
+
+ val redLauncher = registerForActivityResult(RedContract()) {
+ onResult(it)
+ }
+
+ val blueLauncher = registerForActivityResult(BlueContract()) {
+ onResult(it)
+ }
+
+ val greenLauncher = registerForActivityResult(GreenContract()) {
+ onResult(it)
+ }
+
+ when (intent?.component?.className) {
+ "com.android.onboarding.nodes.testing.testapp.RedActivity" -> {
+ findViewById<LinearLayout>(R.id.bg).setBackgroundColor(Color.RED)
+ RedContract().validate(this, intent)
+ }
+ "com.android.onboarding.nodes.testing.testapp.BlueActivity" -> {
+ findViewById<LinearLayout>(R.id.bg).setBackgroundColor(Color.BLUE)
+ BlueContract().validate(this, intent)
+ }
+ "com.android.onboarding.nodes.testing.testapp.GreenActivity" -> {
+ findViewById<LinearLayout>(R.id.bg).setBackgroundColor(Color.GREEN)
+ GreenContract().validate(this, intent)
+ }
+ }
+
+ findViewById<Button>(R.id.redButton).setOnClickListener {
+ redLauncher.launch(10)
+ }
+ findViewById<Button>(R.id.blueButton).setOnClickListener {
+ blueLauncher.launch(10)
+ }
+ findViewById<Button>(R.id.greenButton).setOnClickListener {
+ greenLauncher.launch(10)
+ }
+ }
+
+ fun onResult(result: Int) {
+
+ }
+}
+
+class RedContract : OnboardingActivityApiContract<Int, Int>() {
+ override fun performCreateIntent(context: Context, arg: Int): Intent = Intent("com.android.onboarding.nodes.testing.testapp.red").apply {
+ setComponent(ComponentName(context.packageName, "com.android.onboarding.nodes.testing.testapp.RedActivity"))
+ putExtra("KEY", arg)
+ }
+
+ override fun performExtractArgument(intent: Intent): Int = intent.getIntExtra("KEY", -1)
+
+ override fun performParseResult(result: ContractResult): Int = result.resultCode
+
+ override fun performSetResult(result: Int): ContractResult = ContractResult.Success(result)
+}
+
+class BlueContract : OnboardingActivityApiContract<Int, Int>() {
+ override fun performCreateIntent(context: Context, arg: Int): Intent = Intent("com.android.onboarding.nodes.testing.testapp.blue").apply {
+ setComponent(ComponentName(context.packageName, "com.android.onboarding.nodes.testing.testapp.BlueActivity"))
+ putExtra("KEY", arg)
+ }
+
+ override fun performExtractArgument(intent: Intent): Int = intent.getIntExtra("KEY", -1)
+
+ override fun performParseResult(result: ContractResult): Int = result.resultCode
+
+ override fun performSetResult(result: Int): ContractResult = ContractResult.Success(result)
+}
+
+class GreenContract : OnboardingActivityApiContract<Int, Int>() {
+ override fun performCreateIntent(context: Context, arg: Int): Intent = Intent("com.android.onboarding.nodes.testing.testapp.green").apply {
+ setComponent(ComponentName(context.packageName, "com.android.onboarding.nodes.testing.testapp.GreenActivity"))
+ putExtra("KEY", arg)
+ }
+
+ override fun performExtractArgument(intent: Intent): Int = intent.getIntExtra("KEY", -1)
+
+ override fun performParseResult(result: ContractResult): Int = result.resultCode
+
+ override fun performSetResult(result: Int): ContractResult = ContractResult.Success(result)
+}
diff --git a/src/com/android/onboarding/nodes/testing/testapp/res/layout/activity_main.xml b/src/com/android/onboarding/nodes/testing/testapp/res/layout/activity_main.xml
new file mode 100644
index 0000000..a63b138
--- /dev/null
+++ b/src/com/android/onboarding/nodes/testing/testapp/res/layout/activity_main.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/bg"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <Button
+ android:id="@+id/redButton"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:text="Launch Red" />
+
+ <Button
+ android:id="@+id/blueButton"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:text="Launch Blue" />
+
+ <Button
+ android:id="@+id/greenButton"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:text="Launch Green" />
+
+ <Button
+ android:id="@+id/visualiseButton"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:text="Visualise" />
+</LinearLayout>
diff --git a/src/com/android/onboarding/nodes/visualiser/Android.bp b/src/com/android/onboarding/nodes/visualiser/Android.bp
deleted file mode 100644
index db5877f..0000000
--- a/src/com/android/onboarding/nodes/visualiser/Android.bp
+++ /dev/null
@@ -1,22 +0,0 @@
-package {
- default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-java_library_host {
- name: "android_onboarding.nodes.visualiser",
- installable: false,
- srcs: [
- "*.kt",
- ],
- static_libs: [
- "android_onboarding.nodes_host",
- ],
-}
-
-java_binary_host {
- name: "android_onboarding.nodes.visualiser_png",
- main_class: "com.android.onboarding.nodes.visualiser.PngOnboardingGraphVisualiser",
- static_libs: [
- "android_onboarding.nodes.visualiser"
- ],
-}
diff --git a/src/com/android/onboarding/nodes/visualiser/PngOnboardingGraphVisualiser.kt b/src/com/android/onboarding/nodes/visualiser/PngOnboardingGraphVisualiser.kt
deleted file mode 100644
index 2e938c7..0000000
--- a/src/com/android/onboarding/nodes/visualiser/PngOnboardingGraphVisualiser.kt
+++ /dev/null
@@ -1,266 +0,0 @@
-package com.android.onboarding.nodes.visualiser
-
-import com.android.onboarding.nodes.OnboardingGraph
-import java.awt.Color
-import java.awt.Font
-import java.awt.Graphics2D
-import java.awt.Polygon
-import java.awt.Rectangle
-import java.awt.geom.Line2D
-import java.awt.image.BufferedImage
-import java.lang.IllegalStateException
-import java.time.Duration
-import java.time.Instant
-import java.time.ZoneId
-import java.time.format.DateTimeFormatter
-import java.time.temporal.ChronoUnit
-
-/** Visualiser which produces a PNG image representing an [OnboardingGraph]. */
-object PngOnboardingGraphVisualiser {
-
- /** The number of seconds to pad to the left and right of the timeline. */
- private const val SECONDS_PADDING = 5L
- /** The number of pixels used per second of time on the timeline. */
- private const val PIXELS_PER_SECOND = 50
- /** The number of seconds between each vertical line on the timeline. */
- private const val SECONDS_PER_LINE = 1L
- /** The Y co-ordinate where we will start our first node. */
- private const val GRAPH_VERTICAL_PADDING = 70
- /** The padding beneath a node before we start the next level of embed. */
- private const val NODE_VERTICAL_PADDING = 50
- /** The height of each node. */
- private const val NODE_HEIGHT = 100
-
- /** An arrow which can be printed on the png. */
- private val arrowHead =
- Polygon().apply {
- addPoint(0, 5)
- addPoint(-5, -5)
- addPoint(5, -5)
- }
-
- /** A class used to track general data about the graph we are visualising. */
- data class GraphData(
- /** The earliest timestamp present on the graph. */
- val earliestTimestamp: Instant,
- /** The latest timestamp present on the graph. */
- val latestTimestamp: Instant,
- /** The maximum level any single node is embedded. A flat graph will have a value of 0. */
- val maxNodeDepth: Int,
-
- /** The width (in pixels) of the graph. */
- val width: Int,
- /** The height (in pixels) of the graph. */
- val height: Int
- ) {
-
- /** Gets the pixel which corresponds to a particular time on the timeline. */
- fun instantToPixel(instant: Instant) =
- (PIXELS_PER_SECOND * Duration.between(earliestTimestamp, instant).seconds).toInt()
- }
-
- /** Extracts graph data from an [OnboardingGraph]. */
- private fun extractGraphData(graph: OnboardingGraph): GraphData {
- var earliestTimestamp = Instant.MAX
- var latestTimestamp = Instant.MIN
-
- for (node in graph.nodes.values) {
- node.start.takeIf { it.isBefore(earliestTimestamp) }?.let { earliestTimestamp = it }
- node.end.takeIf { it.isAfter(latestTimestamp) }?.let { latestTimestamp = it }
- }
-
- // Then we apply padding
- earliestTimestamp =
- earliestTimestamp.minusSeconds(SECONDS_PADDING).truncatedTo(ChronoUnit.SECONDS)
- latestTimestamp = latestTimestamp.plusSeconds(SECONDS_PADDING).truncatedTo(ChronoUnit.SECONDS)
-
- val maxNodeDepth =
- graph.nodes.values.maxOfOrNull { it.embedLevel }
- ?: throw IllegalStateException("Cannot render graph with no nodes")
-
- val duration = Duration.between(earliestTimestamp, latestTimestamp)
- val width = (duration.seconds * PIXELS_PER_SECOND).toInt()
- val height =
- ((maxNodeDepth + 1) * (NODE_HEIGHT + NODE_VERTICAL_PADDING)) + (GRAPH_VERTICAL_PADDING * 2)
-
- return GraphData(earliestTimestamp, latestTimestamp, maxNodeDepth, width, height)
- }
-
- /** Returns a [BufferedImage] of a rendering of the [OnboardingGraph]. */
- fun visualise(graph: OnboardingGraph): BufferedImage {
- val graphData = extractGraphData(graph)
-
- val bufferedImage = BufferedImage(graphData.width, graphData.height, BufferedImage.TYPE_INT_RGB)
- val g2d = bufferedImage.createGraphics()
-
- g2d.setColor(Color.white)
- g2d.fillRect(0, 0, graphData.width, graphData.height)
-
- drawTimeline(g2d, graphData)
- drawNodes(g2d, graphData, graph)
-
- g2d.dispose()
-
- return bufferedImage
- }
-
- /**
- * Draw the timeline for the graph.
- *
- * This is the timestamp labels and the ticks.
- */
- fun drawTimeline(g2d: Graphics2D, graphData: GraphData) {
- val formatter = DateTimeFormatter.ofPattern("H:m:s").withZone(ZoneId.systemDefault())
- val font = Font(g2d.font.toString(), 0, 12)
-
- g2d.setColor(Color.gray)
- g2d.fillRect(0, 40, graphData.width, 4)
- var time = graphData.earliestTimestamp
- while (time <= graphData.latestTimestamp) {
- val pixelsFromLeft = graphData.instantToPixel(time)
-
- g2d.fillRect(pixelsFromLeft, 40, 1, 600)
-
- drawCenteredString(
- g2d,
- formatter.format(time),
- Rectangle(pixelsFromLeft - (PIXELS_PER_SECOND), 20, PIXELS_PER_SECOND * 2, 20),
- font
- )
-
- time = time.plusSeconds(SECONDS_PER_LINE)
- }
- }
-
- /** Draw nodes in the graph and connections between them. */
- private fun drawNodes(g2d: Graphics2D, graphData: GraphData, graph: OnboardingGraph) {
- val font = Font(g2d.font.toString(), 0, 12)
-
- for (node in graph.nodes.values) {
- val embed = node.embedLevel
-
- // We offset the start by half a second to make transitions visible
- var startXpixel = graphData.instantToPixel(node.start) + (PIXELS_PER_SECOND / 2)
- var endXpixel = graphData.instantToPixel(node.end)
- var startYpixel = GRAPH_VERTICAL_PADDING + embed * (NODE_HEIGHT + NODE_VERTICAL_PADDING)
-
- g2d.setColor(Color.white)
- g2d.fillRect(startXpixel, startYpixel, endXpixel - startXpixel, NODE_HEIGHT)
-
- g2d.setColor(Color.black)
- g2d.drawRect(startXpixel, startYpixel, endXpixel - startXpixel, NODE_HEIGHT)
-
- drawCenteredString(
- g2d,
- node.name,
- Rectangle(startXpixel, startYpixel, endXpixel - startXpixel, NODE_HEIGHT),
- font
- )
-
- // If there is an incoming edge - draw it
- node.incomingEdge?.let {
- if (it.node.end <= node.start) {
- // The incoming node ended when starting this one - so no need to embed further
-
- val transitionXStart = graphData.instantToPixel(it.timestamp).toDouble()
- val transitionXEnd =
- graphData.instantToPixel(it.timestamp).toDouble() + (PIXELS_PER_SECOND / 2)
-
- // Draw a right-facing arrow between the nodes
- drawArrowLine(
- g2d,
- Line2D.Double(
- transitionXStart,
- (GRAPH_VERTICAL_PADDING.toDouble() +
- (embed) * (NODE_HEIGHT + NODE_VERTICAL_PADDING)) + (NODE_HEIGHT / 2),
- transitionXEnd - 5, // Slightly to the left of the start to leave room for the arrow
- (GRAPH_VERTICAL_PADDING.toDouble() +
- (embed) * (NODE_HEIGHT + NODE_VERTICAL_PADDING)) + (NODE_HEIGHT / 2)
- )
- )
- } else {
- // The incoming node is still running - so we need to draw this node below the other one
-
- val transitionX =
- graphData.instantToPixel(it.timestamp).toDouble() + (PIXELS_PER_SECOND / 2)
- val returnTimeX = graphData.instantToPixel(node.end).toDouble()
-
- // Draw line showing the node start
- drawArrowLine(
- g2d,
- Line2D.Double(
- transitionX,
- (GRAPH_VERTICAL_PADDING.toDouble() +
- (embed - 1) * (NODE_HEIGHT + NODE_VERTICAL_PADDING)) + NODE_HEIGHT,
- transitionX,
- (startYpixel - 5)
- .toDouble() // Slightly above the start of this node to leave room for the arrow
- )
- )
-
- // Draw line showing the return
- drawArrowLine(
- g2d,
- Line2D.Double(
- returnTimeX,
- startYpixel.toDouble(),
- returnTimeX,
- (GRAPH_VERTICAL_PADDING.toDouble() +
- (embed - 1) * (NODE_HEIGHT + NODE_VERTICAL_PADDING)) +
- NODE_HEIGHT +
- 5 // Slightly below the end of the previous node to leave room for the arrow
- )
- )
- }
- }
- }
- }
-
- /** Draw a line with an arrow on the end. */
- private fun drawArrowLine(g2d: Graphics2D, line: Line2D.Double) {
- g2d.drawLine(line.x1.toInt(), line.y1.toInt(), line.x2.toInt(), line.y2.toInt())
- var tx = g2d.getTransform()
- val angle = Math.atan2(line.y2 - line.y1, line.x2 - line.x1)
- tx.translate(line.x2, line.y2)
- tx.rotate(angle - Math.PI / 2.0)
- val g: Graphics2D = g2d.create() as Graphics2D
- g.setTransform(tx)
- g.fill(arrowHead)
- g.dispose()
- }
-
- /** Draw a string centered in the provided [rect]. */
- private fun drawCenteredString(g: Graphics2D, text: String, rect: Rectangle, font: Font) {
- val metrics = g.getFontMetrics(font)
- val stringWidth = metrics.stringWidth(text)
- val stringHeight = metrics.getHeight()
-
- val x: Int = rect.x + (rect.width - stringWidth) / 2
- val y: Int = rect.y + (rect.height - stringHeight) / 2
-
- g.setFont(font)
- g.drawString(text, x, y)
- }
-
- @JvmStatic
- fun main(args: Array<String>) {
- }
-}
-
-val OnboardingGraph.Node.embedLevel: Int
- get() {
- var embed = 0
-
- var n: OnboardingGraph.Node? = this
- while (n != null) {
- if (n.incomingEdge?.node?.end?.let { it <= n!!.start } != false) {
- return embed
- }
-
- n = n.incomingEdge?.node ?: return embed
-
- embed += 1
- }
-
- return embed
- }
diff --git a/src/com/android/onboarding/testing/Android.bp b/src/com/android/onboarding/testing/Android.bp
new file mode 100644
index 0000000..c275f03
--- /dev/null
+++ b/src/com/android/onboarding/testing/Android.bp
@@ -0,0 +1,18 @@
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_library {
+ name: "android_onboarding.testing",
+ manifest: ":android_onboarding.AndroidManifest",
+ srcs: [
+ "*.kt",
+ ],
+ dont_merge_manifests: true,
+ static_libs: [
+ "error_prone_annotations",
+ "androidx.test.core",
+ "Robolectric_all-target",
+ "truth",
+ ],
+}
diff --git a/src/com/android/onboarding/testing/ErrorSubject.kt b/src/com/android/onboarding/testing/ErrorSubject.kt
new file mode 100644
index 0000000..d336e1c
--- /dev/null
+++ b/src/com/android/onboarding/testing/ErrorSubject.kt
@@ -0,0 +1,63 @@
+package com.android.onboarding.testing
+
+import com.google.common.truth.Fact.fact
+import com.google.common.truth.Fact.simpleFact
+import com.google.common.truth.FailureMetadata
+import com.google.common.truth.StandardSubjectBuilder
+import com.google.common.truth.Subject
+import com.google.common.truth.ThrowableSubject
+import com.google.common.truth.Truth
+import com.google.common.truth.Truth.assertAbout
+import com.google.common.truth.Truth.assertThat
+import com.google.errorprone.annotations.CanIgnoreReturnValue
+import kotlin.reflect.KClass
+
+/** A [Truth] [Subject] to assert expected [Throwable]s being thrown. */
+class ErrorSubject private constructor(metadata: FailureMetadata, private val actual: () -> Any?) :
+ Subject(metadata, actual) {
+
+ @CanIgnoreReturnValue
+ inline fun <reified T : Throwable> failsWith(): ThrowableSubject = failsWith(T::class)
+
+ @CanIgnoreReturnValue
+ fun <T : Throwable> failsWith(expected: KClass<T>): ThrowableSubject {
+ val error = runCatching(actual).exceptionOrNull()
+ if (error == null) {
+ failWithoutActual(
+ fact("expected to fail with", expected.qualifiedName),
+ simpleFact("but did not fail"),
+ )
+ } else if (!expected.java.isInstance(error)) {
+ failWithoutActual(
+ fact("expected to fail with", expected.qualifiedName),
+ fact("but failed with", error::class.qualifiedName),
+ )
+ }
+ return assertThat(error)
+ }
+
+ companion object : Factory<ErrorSubject, () -> Any?> {
+ override fun createSubject(metadata: FailureMetadata, actual: (() -> Any?)?): ErrorSubject =
+ ErrorSubject(metadata, actual ?: error("Unable to assert empty lambda"))
+ }
+}
+
+/** @see assertFailsWith */
+inline fun <reified T : Throwable> StandardSubjectBuilder.failsWith(noinline action: () -> Any?) {
+ about(ErrorSubject).that(action).failsWith<T>()
+}
+
+/**
+ * Unfortunately @[CanIgnoreReturnValue] is not supported on inline library functions and as such
+ * this utility function is not returning [ThrowableSubject] for further assertions. If you need
+ * further assertions consider using one of the more verbose forms:
+ * ```
+ * @get:Rule val expect: Expect = Expect.create()
+ * expect.about(ErrorSubject).that { functionUnderTest() }.failsWith<Throwable>().isNotNull()
+ *
+ * assertAbout(ErrorSubject).that { functionUnderTest() }.failsWith<Throwable>().isNotNull()
+ * ```
+ */
+inline fun <reified T : Throwable> assertFailsWith(noinline action: () -> Any?) {
+ assertAbout(ErrorSubject).that(action).failsWith<T>()
+}