diff options
author | Martynas Petuška <petuska@google.com> | 2023-10-03 12:29:04 +0000 |
---|---|---|
committer | Martynas Petuška <petuska@google.com> | 2023-10-03 14:26:01 +0000 |
commit | 037c6b4c9f1455ad138c43f76ceb7fbf0e26bae8 (patch) | |
tree | d7b76defba70bb722b8370621d7c5c17504b16b6 | |
parent | 6a5e800fa2f88bb970ab7639419a95f6a4196b80 (diff) | |
download | android_onboarding-037c6b4c9f1455ad138c43f76ceb7fbf0e26bae8.tar.gz |
external/android_onboarding: Copybara import of Android Onboarding
Squash update.
PiperOrigin-RevId: 570356198
Change-Id: Iad8ad832f702dea94598979f0b00eb46e4d0d05d
38 files changed, 794 insertions, 517 deletions
@@ -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>() +} |