From 1c807685ca73e243b230e5e16cc1f2cfdb8a87c6 Mon Sep 17 00:00:00 2001 From: android-devrel-github-bot Date: Mon, 24 Aug 2026 08:35:55 +0000 Subject: [PATCH] Updates skills (2026-08-24 08:35) --- .claude-plugin/marketplace.json | 1 + .codex-plugin/plugin.json | 5 + build-system/agp/agp-9-upgrade/SKILL.md | 34 +- .../build/migrate-to-built-in-kotlin.md | 104 +- .../build/releases/agp-9-0-0-release-notes.md | 309 +++-- .../agp-9-upgrade/references/buildconfig.md | 13 +- .../agp/agp-9-upgrade/references/ksp-kapt.md | 30 +- .../references/paparazzi-gradle-9.md | 17 +- .../agp/agp-9-upgrade/references/recipes.md | 18 +- camera/camerax/SKILL.md | 26 +- .../camerax/references/camera1-to-camerax.md | 27 +- .../camerax/references/camera2-to-camerax.md | 41 +- .../camerax/references/expert-blueprints.md | 7 +- camera/camerax/references/foldables.md | 21 +- camera/camerax/references/immutability.md | 11 +- camera/camerax/references/low-light.md | 16 +- camera/camerax/references/mlkit-spatial.md | 22 +- camera/camerax/references/modern-apis.md | 6 +- camera/camerax/references/testing.md | 21 +- camera/camerax/references/thermals.md | 14 +- camera/camerax/references/wear-os.md | 21 +- camera/camerax/references/xr.md | 10 +- device-ai/appfunctions/SKILL.md | 22 +- .../references/adb-interaction-testing.md | 35 +- device-ai/appfunctions/references/context.md | 21 +- .../references/feature-discovery-analysis.md | 33 +- .../implementation-configuration.md | 53 +- .../kdoc-refinement-optimization.md | 16 +- .../migrate-to-service-entry-point.md | 71 +- identity/restore-credentials/SKILL.md | 340 +++++ .../topics/manifest/application-element.md | 832 ++++++++++++ .../identity/passkeys/create-passkeys.md | 327 +++++ .../passkeys/sign-in-with-passkeys.md | 216 ++++ identity/restore-credentials/skill.md | 319 +++++ identity/verified-email/SKILL.md | 121 +- .../identity/credential-manager/index.md | 35 +- .../credential-verifier.md | 29 +- .../email-verification-implementation.md | 99 +- .../digital-credentials/email-verification.md | 78 +- .../identity/digital-credentials/index.md | 60 +- .../identity/passkeys/create-passkeys.md | 113 +- .../sign-in/credential-manager-webview.md | 27 +- jetpack-compose/adaptive/SKILL.md | 96 +- .../adaptive/flexbox/container-behavior.md | 49 +- .../layouts/adaptive/flexbox/get-started.md | 19 +- .../compose/layouts/adaptive/flexbox/index.md | 47 +- .../layouts/adaptive/flexbox/item-behavior.md | 65 +- .../adaptive/grid/container-properties.md | 191 ++- .../layouts/adaptive/grid/get-started.md | 13 +- .../ui/compose/layouts/adaptive/grid/index.md | 40 +- .../layouts/adaptive/grid/item-properties.md | 60 +- .../layouts/adaptive/mediaquery/index.md | 142 ++- .../develop/ui/compose/tooling/debug.md | 78 +- .../recipes/material-listdetail.md | 2 - .../SKILL.md | 63 +- .../analysis-of-the-project-and-layout.md | 29 +- .../migrate-xml-theme-to-compose.md | 50 +- .../interoperability-apis/compose-in-views.md | 79 +- .../interoperability-apis/views-in-compose.md | 92 +- ...setup-compose-dependencies-and-compiler.md | 45 +- .../identify-optimal-xml-candidate.md | 10 +- .../references/xml-layout-migration.md | 12 +- jetpack-compose/theming/styles/SKILL.md | 40 +- .../ui/compose/designsystems/custom.md | 111 +- .../develop/ui/compose/styles/fundamentals.md | 110 +- .../ui/compose/styles/state-animations.md | 88 +- .../ui/compose/styles/styles-vs-modifiers.md | 17 +- .../develop/ui/compose/styles/theming.md | 67 +- media/media3-cast-integration/SKILL.md | 29 +- .../media/media3/cast/create-castplayer.md | 111 +- .../media3/cast/customize-castoptions.md | 21 +- .../android/media/media3/cast/index.md | 14 +- navigation/navigation-3/SKILL.md | 2 +- .../guide/navigation/navigation-3/index.md | 22 +- .../navigation-3/migration-guide.md | 530 +++++--- .../navigation-3/recipes/animations.md | 2 - .../navigation/navigation-3/recipes/basic.md | 2 - .../navigation-3/recipes/basicdsl.md | 2 - .../navigation-3/recipes/basicsaveable.md | 2 - .../navigation-3/recipes/bottomsheet.md | 3 - .../navigation-3/recipes/common-ui.md | 2 - .../recipes/conditional-transitions.md | 2 - .../navigation-3/recipes/conditional.md | 2 - .../recipes/deeplinks-custommatcher.md | 10 +- .../recipes/deeplinks-staticuri.md | 3 - .../recipes/deeplinks-syntheticbackstack.md | 25 +- .../recipes/deeplinks-uriarguments.md | 6 - .../navigation/navigation-3/recipes/dialog.md | 2 - .../navigation-3/recipes/lifecycle-owner.md | 2 - .../recipes/material-listdetail.md | 2 - .../recipes/material-supportingpane.md | 2 - .../navigation-3/recipes/modular-hilt.md | 9 - .../navigation-3/recipes/modular-koin.md | 8 - .../recipes/multiple-backstacks.md | 8 - .../navigation-3/recipes/passingarguments.md | 5 - .../navigation-3/recipes/results-event.md | 6 - .../navigation-3/recipes/results-state.md | 6 - .../navigation-3/recipes/scenes-listdetail.md | 4 - .../navigation-3/recipes/scenes-twopane.md | 4 - .../navigation/type-safe-destinations.md | 21 +- performance/r8-analyzer/SKILL.md | 5 +- .../references/CONFIGURATION-ANALYZER.md | 36 +- .../r8-analyzer/references/CONFIGURATION.md | 11 +- .../references/KEEP-RULES-IMPACT-HIERARCHY.md | 40 +- .../r8-analyzer/references/REDUNDANT-RULES.md | 109 +- .../references/REFLECTION-GUIDE.md | 108 +- .../r8-analyzer/references/REPORT_FORMAT.md | 14 +- .../enable-app-optimization.md | 122 +- .../testing/other-components/ui-automator.md | 106 +- play/engage-sdk-integration/SKILL.md | 30 +- .../android/guide/playcore/engage/faq.md | 215 +++- .../android/guide/playcore/engage/food.md | 221 +++- .../guide/playcore/engage/healthandfitness.md | 231 +++- .../android/guide/playcore/engage/listen.md | 209 ++- .../guide/playcore/engage/otherverticals.md | 263 +++- .../android/guide/playcore/engage/read.md | 192 ++- .../android/guide/playcore/engage/shopping.md | 252 +++- .../android/guide/playcore/engage/social.md | 142 ++- .../android/guide/playcore/engage/travel.md | 276 +++- .../engage/tv/continue-watching/index.md | 15 +- .../guide/playcore/engage/tv/entitlements.md | 101 +- .../playcore/engage/tv/getting-started.md | 74 +- .../playcore/engage/tv/recommendations.md | 114 +- .../android/guide/playcore/engage/watch.md | 203 ++- .../references/clusters.md | 13 +- .../references/patterns.md | 48 +- .../references/requests.md | 49 +- .../references/troubleshooting.md | 39 + .../SKILL.md | 22 +- .../google/play/billing/release-notes.md | 577 +++++++-- .../references/migration-logic.md | 20 +- .../references/version-checklist.md | 3 +- security/android-intent-security/SKILL.md | 83 +- system/edge-to-edge/SKILL.md | 82 +- testing/testing-setup/SKILL.md | 86 +- .../ui/compose/testing/common-patterns.md | 72 +- .../preview/compose-screenshot-testing.md | 93 +- .../dependency-injection/hilt-testing.md | 156 ++- tv/leanback-to-compose-tv-migration/SKILL.md | 136 +- .../training/tv/get-started/navigation.md | 256 +++- .../training/tv/playback/compose/browse.md | 290 +++-- .../training/tv/playback/compose/index.md | 64 +- wear/wear-compose-m3/SKILL.md | 29 +- .../wearables/compose/migrate-to-material3.md | 170 ++- .../references/material3/CardSample.kt.md.txt | 2 +- .../DynamicColorSchemeSample.kt.md.txt | 2 +- .../SKILL.md | 99 +- .../ai-glasses/guides/interaction/inputs.md | 16 +- .../access-hardware-projected-context.md | 139 +- .../jetpack-compose-glimmer/buttons.md | 88 +- .../jetpack-compose-glimmer/cards.md | 128 +- .../jetpack-compose-glimmer/focus.md | 49 +- .../jetpack-compose-glimmer/icons.md | 83 +- .../jetpack-compose-glimmer/text.md | 62 +- .../jetpack-compose-glimmer/title-chips.md | 38 +- .../request-hardware-permissions.md | 93 +- .../references/button-samples-source.md | 6 +- .../references/button-source.md | 10 +- .../references/card-samples-source.md | 6 +- .../references/card-source.md | 278 ++-- .../references/deptheffect-source.md | 6 +- .../references/deptheffectlevels-source.md | 6 +- .../glimmersansflextypography-source.md | 6 +- .../references/glimmertheme-source.md | 6 +- .../references/icon-source.md | 6 +- .../references/iconbutton-source.md | 10 +- .../references/iconsizes-source.md | 6 +- .../references/list-source.md | 6 +- .../references/listitem-source.md | 15 +- .../references/liststate-source.md | 6 +- .../references/material-hct-source.md | 2 - .../references/projectedcontext-source.md | 18 +- .../references/stack-source.md | 6 +- .../references/stackitemscope-source.md | 6 +- .../references/stackstate-source.md | 6 +- .../references/surface-source.md | 1129 ++++++++++++----- .../references/titlechip-samples-source.md | 6 +- .../references/titlechip-source.md | 10 +- 178 files changed, 10222 insertions(+), 3615 deletions(-) create mode 100644 identity/restore-credentials/SKILL.md create mode 100644 identity/restore-credentials/references/android/guide/topics/manifest/application-element.md create mode 100644 identity/restore-credentials/references/android/identity/passkeys/create-passkeys.md create mode 100644 identity/restore-credentials/references/android/identity/passkeys/sign-in-with-passkeys.md create mode 100644 identity/restore-credentials/skill.md create mode 100644 play/engage-sdk-integration/references/troubleshooting.md diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 679a9fd..b3846d6 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -18,6 +18,7 @@ "./camera/camerax", "./device-ai/appfunctions", "./devtools/android-cli", + "./identity/restore-credentials", "./identity/verified-email", "./jetpack-compose/adaptive", "./jetpack-compose/migration/migrate-xml-views-to-jetpack-compose", diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 0203273..d869e20 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -35,6 +35,11 @@ "path": "./devtools/android-cli" } }, + { + "source": { + "path": "./identity/restore-credentials" + } + }, { "source": { "path": "./identity/verified-email" diff --git a/build-system/agp/agp-9-upgrade/SKILL.md b/build-system/agp/agp-9-upgrade/SKILL.md index 8ad57ba..44f2acf 100644 --- a/build-system/agp/agp-9-upgrade/SKILL.md +++ b/build-system/agp/agp-9-upgrade/SKILL.md @@ -6,7 +6,7 @@ description: Upgrades, or migrates, an Android project to use Android Gradle Plu license: Complete terms in LICENSE.txt metadata: author: Google LLC - last-updated: '2026-08-06' + last-updated: '2026-08-18' keywords: - Android Gradle Plugin 9 - AGP 9 @@ -18,13 +18,23 @@ metadata: ## Migration guide -See the [AGP 9 migration guide](references/android/build/releases/agp-9-0-0-release-notes.md) for the major changes, many breaking, in AGP 9 compared to AGP 8. +See the [AGP 9 migration guide](references/android/build/releases/agp-9-0-0-release-notes.md) for the major changes, many +breaking, in AGP 9 compared to AGP 8. ## Requirements -If the user requests to update or migrate to AGP 9, first check the AGP version used in the project. If it is lower than 9, stop and ask the user to run the AGP Upgrade Assistant in Android Studio to update to the latest stable version of AGP, and confirm when done. The user may also request that this requirement be skipped; if this is the case, you should update the version of AGP to the latest stable version as part of the AGP 9 migration. See the [AGP 9 migration guide](references/android/build/releases/agp-9-0-0-release-notes.md) for how to do this. +If the user requests to update or migrate to AGP 9, first check the AGP version +used in the project. If it is lower than 9, stop and ask the user to run the AGP +Upgrade Assistant in Android Studio to update to the latest stable version of +AGP, and confirm when done. The user may also request that this requirement be +skipped; if this is the case, you should update the version of AGP to the latest +stable version as part of the AGP 9 migration. See the +[AGP 9 migration guide](references/android/build/releases/agp-9-0-0-release-notes.md) for how to do this. -Each version of AGP has its own set of compatibilities with other tools, such as Gradle, JDK, and Kotlin. The release notes for each of these versions will include a **Compatibility** table indicating the minimum versions for these tools. +Each version of AGP has its own set of compatibilities with other tools, such as +Gradle, JDK, and Kotlin. The release notes for each of these versions will +include a **Compatibility** table indicating the minimum versions for these +tools. Do not use this skill for KMP projects, as they are unsupported. @@ -34,7 +44,8 @@ If AGP is already at 9 or higher, then do the following: ### Step 1: Update dependencies -If KSP (`com.google.devtools.ksp`) is used in the project, ensure it is on version 2.3.6 or higher. +If KSP (`com.google.devtools.ksp`) is used in the project, ensure it is on +version 2.3.6 or higher. If Hilt is used in the project, ensure it is on version 2.59.2 or higher. @@ -46,15 +57,19 @@ See [the guide](references/android/build/migrate-to-built-in-kotlin.md) for deta See [the guide](references/android/build/releases/agp-9-0-0-release-notes.md) for detailed information. -See also [gradle-recipes](references/recipes.md) for examples on how to migrate old code to code that is compatible with AGP 9 and the new DSL. +See also [gradle-recipes](references/recipes.md) for examples on how to migrate old code to code +that is compatible with AGP 9 and the new DSL. ### Step 4. Migrate kapt to KSP or legacy-kapt -If KSP (`com.google.devtools.ksp`) or kapt (`org.jetbrains.kotlin.kapt`) are used in the project, see [KSP, kapt, and legacy-kapt](references/ksp-kapt.md) for detailed migration steps. +If KSP (`com.google.devtools.ksp`) or kapt (`org.jetbrains.kotlin.kapt`) are +used in the project, see [KSP, kapt, and legacy-kapt](references/ksp-kapt.md) for detailed migration +steps. ### Step 5. BuildConfig -If any Android module contains custom BuildConfig fields, see [BuildConfig](references/buildconfig.md) for detailed information. +If any Android module contains custom BuildConfig fields, see [BuildConfig](references/buildconfig.md) +for detailed information. ### Step 6. Update gradle.properties @@ -84,4 +99,5 @@ After migration, verify the following: ## Troubleshooting -Paparazzi v2.0.0-alpha04 and lower versions have issues with AGP 9. See [references/paparazzi-gradle-9.md](references/paparazzi-gradle-9.md) for details. +Paparazzi v2.0.0-alpha04 and lower versions have issues with AGP 9. See +[references/paparazzi-gradle-9.md](references/paparazzi-gradle-9.md) for details. diff --git a/build-system/agp/agp-9-upgrade/references/android/build/migrate-to-built-in-kotlin.md b/build-system/agp/agp-9-upgrade/references/android/build/migrate-to-built-in-kotlin.md index 820bf0f..7306e7b 100644 --- a/build-system/agp/agp-9-upgrade/references/android/build/migrate-to-built-in-kotlin.md +++ b/build-system/agp/agp-9-upgrade/references/android/build/migrate-to-built-in-kotlin.md @@ -1,20 +1,33 @@ -Android Gradle plugin 9.0 introduces built-in Kotlin support and enables it by default. That means you no longer have to apply the `org.jetbrains.kotlin.android` (or `kotlin-android`) plugin in your build files to compile Kotlin source files. With built-in Kotlin, your build files are simpler and you can avoid compatibility issues between AGP and the `kotlin-android` plugin. +Android Gradle plugin 9.0 introduces built-in Kotlin support and enables it +by default. That means you no longer have to apply the +`org.jetbrains.kotlin.android` (or `kotlin-android`) plugin in your build files +to compile Kotlin source files. +With built-in Kotlin, your build files are simpler and you can avoid +compatibility issues between AGP and the `kotlin-android` plugin. > [!NOTE] > **Note:** Built-in Kotlin replaces the `kotlin-android` plugin only. If you are writing a Kotlin Multiplatform (KMP) library module, you still need to apply the `org.jetbrains.kotlin.multiplatform` plugin and the [`com.android.kotlin.multiplatform.library`](https://developer.android.com/kotlin/multiplatform/plugin) plugin. Also, using the `org.jetbrains.kotlin.multiplatform` plugin together with the `com.android.library` or `com.android.application` plugin is no longer allowed when built-in Kotlin is enabled. ## Enable built-in Kotlin -You need AGP 9.0 or higher to have built-in Kotlin support. AGP 9.0 already enables built-in Kotlin for all your modules where you apply AGP, so you don't need to do anything to enable it. However, if you previously [opted out of built-in Kotlin](https://developer.android.com/build/migrate-to-built-in-kotlin#opt-out-of-built-in-kotlin) by setting `android.builtInKotlin=false` in the `gradle.properties` file, you need to remove that setting or set it to `true`. +You need AGP 9.0 or higher to have built-in Kotlin support. +AGP 9.0 already enables built-in Kotlin for all your modules where you apply +AGP, so you don't need to do anything to enable it. However, if you previously +[opted out of built-in Kotlin](https://developer.android.com/build/migrate-to-built-in-kotlin#opt-out-of-built-in-kotlin) by setting `android.builtInKotlin=false` +in the `gradle.properties` file, you need to remove that setting or set it to +`true`. > [!NOTE] > **Note:** You can also enable built-in Kotlin for [one module at a time](https://developer.android.com/build/migrate-to-built-in-kotlin#module-by-module-migration). -Built-in Kotlin requires some changes to your project, so after you have built-in Kotlin enabled, follow the next steps to migrate your project. +Built-in Kotlin requires some changes to your project, so after you +have built-in Kotlin enabled, follow the next steps to migrate your project. ## Migration steps -After you upgrade your project from an older AGP version to AGP 9.0 or after you manually [enable built-in Kotlin](https://developer.android.com/build/migrate-to-built-in-kotlin#enable-built-in-kotlin), you might see the following error message: +After you upgrade your project from an older AGP version to AGP 9.0 or after +you manually [enable built-in Kotlin](https://developer.android.com/build/migrate-to-built-in-kotlin#enable-built-in-kotlin), you might see the following error +message: Failed to apply plugin 'org.jetbrains.kotlin.android'. > Cannot add extension with name 'kotlin', as there is an extension already registered with that name. @@ -24,7 +37,8 @@ After you upgrade your project from an older AGP version to AGP 9.0 or after you Failed to apply plugin 'com.jetbrains.kotlin.android' > The 'org.jetbrains.kotlin.android' plugin is no longer required for Kotlin support since AGP 9.0. -This error occurs because built-in Kotlin requires some changes to your project. To resolve this error, follow these steps: +This error occurs because built-in Kotlin requires some changes to your project. +To resolve this error, follow these steps: > [!NOTE] > **Note:** If you're not yet ready to migrate your project, you can also [opt out of built-in Kotlin](https://developer.android.com/build/migrate-to-built-in-kotlin#opt-out-of-built-in-kotlin). @@ -36,7 +50,10 @@ This error occurs because built-in Kotlin requires some changes to your project. ### 1. Remove the `kotlin-android` plugin -Remove the `org.jetbrains.kotlin.android` (or `kotlin-android`) plugin from the module-level build files where you apply it. The exact code to remove depends on whether you use [version catalogs](https://docs.gradle.org/current/userguide/version_catalogs.html) to declare plugins. +Remove the `org.jetbrains.kotlin.android` (or `kotlin-android`) plugin from +the module-level build files where you apply it. +The exact code to remove depends on +whether you use [version catalogs](https://docs.gradle.org/current/userguide/version_catalogs.html) to declare plugins. ### With version catalogs @@ -120,7 +137,8 @@ plugins { } ``` -If you use version catalogs, also remove the plugin definition from the version catalog TOML file (usually `gradle/libs.versions.toml`): +If you use version catalogs, also remove the plugin definition from the +version catalog TOML file (usually `gradle/libs.versions.toml`): ```toml [plugins] @@ -129,11 +147,16 @@ kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "KOTLIN_VE ### 2. Migrate the `kotlin-kapt` plugin if necessary -The `org.jetbrains.kotlin.kapt` (or `kotlin-kapt`) plugin is incompatible with built-in Kotlin. If you use `kapt`, we recommend that you [migrate your project to KSP](https://developer.android.com/build/migrate-to-ksp). +The `org.jetbrains.kotlin.kapt` (or `kotlin-kapt`) plugin is incompatible with +built-in Kotlin. If you use `kapt`, we recommend that you +[migrate your project to KSP](https://developer.android.com/build/migrate-to-ksp). -If you can't migrate to KSP yet, replace the `kotlin-kapt` plugin with the `com.android.legacy-kapt` plugin, using the same version as your Android Gradle plugin. +If you can't migrate to KSP yet, replace the `kotlin-kapt` plugin with the +`com.android.legacy-kapt` plugin, using the same version as your Android Gradle +plugin. -For example, with version catalogs, update your version catalog TOML file as follows: +For example, with version catalogs, update your version catalog TOML +file as follows: ```toml [plugins] @@ -193,7 +216,8 @@ plugins { ### 3. Migrate the `android.kotlinOptions{}` DSL if necessary -If you use the `android.kotlinOptions{}` DSL, you need to migrate it to the [`kotlin.compilerOptions{}`](https://kotlinlang.org/docs/gradle-compiler-options.html#migrate-from-kotlinoptions-to-compileroptions) DSL. +If you use the `android.kotlinOptions{}` DSL, you need to +migrate it to the [`kotlin.compilerOptions{}`](https://kotlinlang.org/docs/gradle-compiler-options.html#migrate-from-kotlinoptions-to-compileroptions) DSL. For example, update this code: @@ -250,9 +274,15 @@ kotlin { ### 4. Migrate the `kotlin.sourceSets{}` DSL if necessary -When you use the `kotlin-android` plugin, AGP lets you add additional Kotlin source directories using either the [`android.sourceSets{}`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/dsl/AndroidSourceSet) DSL or the [`kotlin.sourceSets{}`](https://kotlinlang.org/api/kotlin-gradle-plugin/kotlin-gradle-plugin-api/org.jetbrains.kotlin.gradle.plugin/-kotlin-source-set/) DSL. With the `android.sourceSets{}` DSL, you can add the directories to either the `AndroidSourceSet.kotlin` set or the `AndroidSourceSet.java` set. +When you use the `kotlin-android` plugin, AGP lets you add additional Kotlin +source directories using either the [`android.sourceSets{}`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/dsl/AndroidSourceSet) DSL or the +[`kotlin.sourceSets{}`](https://kotlinlang.org/api/kotlin-gradle-plugin/kotlin-gradle-plugin-api/org.jetbrains.kotlin.gradle.plugin/-kotlin-source-set/) DSL. +With the `android.sourceSets{}` DSL, you can add the directories to either the +`AndroidSourceSet.kotlin` set or the `AndroidSourceSet.java` set. -With built-in Kotlin, the only supported option is to add the directories to the `AndroidSourceSet.kotlin` set using the `android.sourceSets{}` DSL. If you use unsupported options, migrate them as follows: +With built-in Kotlin, the only supported option is to add the directories to the +`AndroidSourceSet.kotlin` set using the `android.sourceSets{}` DSL. +If you use unsupported options, migrate them as follows: ### Kotlin @@ -292,7 +322,10 @@ android.sourceSets.named("main") { } ``` -If you want to add a Kotlin source directory to a specific variant or if the directory is generated by a task, you can use the [`addStaticSourceDirectory`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/variant/SourceDirectories#addStaticSourceDirectory(kotlin.String)) or [`addGeneratedSourceDirectory`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/variant/SourceDirectories#addGeneratedSourceDirectory(org.gradle.api.tasks.TaskProvider,kotlin.Function1)) methods in the [variant API](https://developer.android.com/build/extend-agp#variant-api-artifacts-tasks): +If you want to add a Kotlin source directory to a specific variant or if the +directory is generated by a task, you can use the +[`addStaticSourceDirectory`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/variant/SourceDirectories#addStaticSourceDirectory(kotlin.String)) or [`addGeneratedSourceDirectory`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/variant/SourceDirectories#addGeneratedSourceDirectory(org.gradle.api.tasks.TaskProvider,kotlin.Function1)) methods +in the [variant API](https://developer.android.com/build/extend-agp#variant-api-artifacts-tasks): ### Kotlin @@ -314,36 +347,57 @@ androidComponents.onVariants { variant -> ## Report issues -If you encounter issues after completing the previous steps, review the known issues in [issue #438678642](https://issuetracker.google.com/438678642) and give us feedback if needed. +If you encounter issues after completing the previous steps, +review the known issues in [issue #438678642](https://issuetracker.google.com/438678642) and give us +feedback if needed. ## Opt out of built-in Kotlin -If you are unable to migrate your project to use built-in Kotlin, set `android.builtInKotlin=false` in the `gradle.properties` file to temporarily disable it. When you do that, the build shows a warning reminding you to migrate to built-in Kotlin as you won't be able to disable built-in Kotlin in AGP 10.0. +If you are unable to migrate your project to use built-in Kotlin, set +`android.builtInKotlin=false` in the `gradle.properties` file to temporarily +disable it. +When you do that, the build shows a warning reminding you to migrate to built-in +Kotlin as you won't be able to disable built-in Kotlin in AGP 10.0. > [!NOTE] > **Note:** You also need to set `android.newDsl=false` to opt out of the [new DSL](https://developer.android.com/r/tools/new-dsl) because the `kotlin-android` plugin is not compatible with it. -Once you're ready to migrate your project, [enable built-in Kotlin](https://developer.android.com/build/migrate-to-built-in-kotlin#enable-built-in-kotlin) and follow the [migration steps](https://developer.android.com/build/migrate-to-built-in-kotlin#migration-steps). +Once you're ready to migrate your project, [enable built-in Kotlin](https://developer.android.com/build/migrate-to-built-in-kotlin#enable-built-in-kotlin) +and follow the [migration steps](https://developer.android.com/build/migrate-to-built-in-kotlin#migration-steps). ## Module-by-module migration -The `android.builtInKotlin` Gradle property lets you enable or disable built-in Kotlin for all your modules where you apply AGP. +The `android.builtInKotlin` Gradle property lets you enable or disable built-in +Kotlin for all your modules where you apply AGP. -If migrating all your modules at once is challenging, you can migrate one module at a time: +If migrating all your modules at once is challenging, you can migrate one module +at a time: -1. Set `android.builtInKotlin=false` in the `gradle.properties` file to disable built-in Kotlin for all modules. +1. Set `android.builtInKotlin=false` in the `gradle.properties` file to + disable built-in Kotlin for all modules. -2. Apply the `com.android.built-in-kotlin` plugin to the module you want to enable built-in Kotlin, using the same version as your Android Gradle plugin. +2. Apply the `com.android.built-in-kotlin` plugin to the module + you want to enable built-in Kotlin, using the same version as your + Android Gradle plugin. -3. Follow the previous [migration steps](https://developer.android.com/build/migrate-to-built-in-kotlin#migration-steps) to migrate this module to built-in Kotlin. +3. Follow the previous [migration steps](https://developer.android.com/build/migrate-to-built-in-kotlin#migration-steps) to migrate this module to + built-in Kotlin. -4. Once you've migrated all your modules, remove the `android.builtInKotlin=false` setting in `gradle.properties` and the `com.android.built-in-kotlin` plugin in your build files. +4. Once you've migrated all your modules, remove the + `android.builtInKotlin=false` setting in `gradle.properties` + and the `com.android.built-in-kotlin` plugin in your build files. ## Option to selectively disable built-in Kotlin -Android Gradle plugin 9.0 enables built-in Kotlin for all modules where it is applied. We recommend disabling built-in Kotlin selectively for modules that don't have Kotlin sources in large projects. This removes both the Kotlin compilation task, which has a small build performance cost, and the automatic dependency on the Kotlin standard library. +Android Gradle plugin 9.0 enables built-in Kotlin for all modules where it is +applied. +We recommend disabling built-in Kotlin selectively for modules that don't have +Kotlin sources in large projects. +This removes both the Kotlin compilation task, which has a small build +performance cost, and the automatic dependency on the Kotlin standard library. -To disable built-in Kotlin for a module, set `enableKotlin = false` in that module's build file: +To disable built-in Kotlin for a module, +set `enableKotlin = false` in that module's build file: ### Kotlin diff --git a/build-system/agp/agp-9-upgrade/references/android/build/releases/agp-9-0-0-release-notes.md b/build-system/agp/agp-9-upgrade/references/android/build/releases/agp-9-0-0-release-notes.md index 40a5d58..90fb17b 100644 --- a/build-system/agp/agp-9-upgrade/references/android/build/releases/agp-9-0-0-release-notes.md +++ b/build-system/agp/agp-9-upgrade/references/android/build/releases/agp-9-0-0-release-notes.md @@ -1,16 +1,25 @@
-Android Gradle plugin 9.0 is a major release that brings API and behavior changes. +Android Gradle plugin 9.0 is a major release that brings API and behavior +changes. -To update to Android Gradle plugin 9.0.1, use the [Android Gradle plugin Upgrade Assistant](https://developer.android.com/build/agp-upgrade-assistant). The AGP upgrade assistant helps preserve existing behaviors when upgrading your project whenever appropriate, so you can upgrade your project to use AGP 9.0 even if you're not ready to adopt all the new defaults in AGP 9.0. +To update to Android Gradle plugin 9.0.1, use the +[Android Gradle plugin Upgrade Assistant](https://developer.android.com/build/agp-upgrade-assistant). The AGP upgrade assistant helps +preserve existing behaviors when upgrading your project whenever appropriate, so +you can upgrade your project to use AGP 9.0 even if you're not ready to +adopt all the new defaults in AGP 9.0. -There are also two agent skills available to make the upgrade process easier. For a non-KMP app, try the [AGP 9 upgrade skill](https://github.com/android/skills/tree/main/build/agp/agp-9-upgrade) from the Android skills repository. For a KMP app, try the [AGP 9 upgrade skill](https://github.com/Kotlin/kotlin-agent-skills/tree/main/skills/kotlin-tooling-agp9-migration) from JetBrains. For more information about using skills in Android Studio, see [Extend Agent Mode with skills](https://developer.android.com/studio/gemini/skills). +There are also two agent skills available to make the upgrade process easier. +For a non-KMP app, try the [AGP 9 upgrade skill](https://github.com/android/skills/tree/main/build-system/agp/agp-9-upgrade) from the +Android skills repository. For a KMP app, try the [AGP 9 upgrade +skill](https://github.com/Kotlin/kotlin-agent-skills/tree/main/skills/kotlin-tooling-agp9-migration) from JetBrains. For more information about using skills +in Android Studio, see [Extend Agent Mode with skills](https://developer.android.com/studio/gemini/skills). ## Compatibility -The maximum API level that Android Gradle plugin 9.0 supports is API level 36.1. Here is other compatibility info: +The maximum API level that Android Gradle plugin 9.0 supports is API level 36.1. +Here is other compatibility info: -
| | Minimum version | Default version | Notes | |---:|:---:|:---:|:---:| @@ -23,25 +32,52 @@ The maximum API level that Android Gradle plugin 9.0 supports is API level 36.1. ## The `android` DSL classes now only implement the new public interfaces -Over the last several years, we have introduced [new interfaces](https://developer.android.com/reference/tools/gradle-api) for our DSL and API in order to better control which APIs are public. AGP versions 7.x and 8.x still used the old DSL types (for example `BaseExtension`) which also implemented the new public interfaces, in order to maintain compatibility as work progressed on the interfaces. +Over the last several years, we have introduced +[new interfaces](https://developer.android.com/reference/tools/gradle-api) for our DSL and API in order to +better control which APIs are public. AGP versions +7.x and 8.x still used the old DSL types (for example `BaseExtension`) which +also implemented the new public interfaces, in order to maintain compatibility +as work progressed on the interfaces. -AGP 9.0 uses our new DSL interfaces exclusively, and the implementations have changed to new types that are fully hidden. This also removes access to the old, deprecated variant API. +AGP 9.0 uses our new DSL interfaces exclusively, +and the implementations have changed to new types that are fully hidden. This +also removes access to the old, deprecated variant API. To update to AGP 9.0, you might need to do the following: -- **Ensure your project is compatible with [built-in Kotlin](https://developer.android.com/build/releases/agp-9-0-0-release-notes#android-gradle-plugin-built-in-kotlin):** The `org.jetbrains.kotlin.android` plugin is not compatible with the new DSL. -- **Switch KMP projects to the [Android Gradle Library Plugin for KMP](https://developer.android.com/kotlin/multiplatform/plugin):** Using the `org.jetbrains.kotlin.multiplatform` plugin in the same Gradle subproject as the `com.android.library` and `com.android.application` plugins is not compatible with the new DSL. +- **Ensure your project is compatible with [built-in + Kotlin](https://developer.android.com/build/releases/agp-9-0-0-release-notes#android-gradle-plugin-built-in-kotlin):** The `org.jetbrains.kotlin.android` plugin is not compatible with the new DSL. +- **Switch KMP projects to the + [Android Gradle Library Plugin for KMP](https://developer.android.com/kotlin/multiplatform/plugin):** + Using the `org.jetbrains.kotlin.multiplatform` plugin in the same Gradle + subproject as the `com.android.library` and `com.android.application` plugins + is not compatible with the new DSL. > [!NOTE] > **Note:** The new KMP integration does not support using KMP and the Android Application plugin in the same Gradle subproject. To migrate, extract your Android app to a separate subproject. -- **Update your build files:** While the change of interfaces is meant to keep the DSL as similar as possible, there might be [some small changes](https://developer.android.com/build/releases/agp-9-0-0-release-notes#android-gradle-plugin-changed-dsl). +- **Update your build files:** + While the change of interfaces is meant to keep + the DSL as similar as possible, there might be + [some small changes](https://developer.android.com/build/releases/agp-9-0-0-release-notes#android-gradle-plugin-changed-dsl). -- **Update your custom build logic to reference the new DSL and API:** Replace any references to the internal DSL with the public DSL interfaces. In most cases this will be a one-to-one replacement. Replace any use of the `applicationVariants` and similar APIs with the new [`androidComponents` API](https://developer.android.com/build/extend-agp#variant-api-artifacts-tasks). This might be more complex, as the `androidComponents` API is designed to be more stable to keep plugins compatible longer. Check our [Gradle Recipes](https://github.com/android/gradle-recipes/tree/agp-9.0) for examples. +- **Update your custom build logic to reference the new DSL and API:** + Replace any references to the internal DSL with the public DSL interfaces. + In most cases this will be a one-to-one replacement. + Replace any use of the `applicationVariants` and similar APIs with the new + [`androidComponents` API](https://developer.android.com/build/extend-agp#variant-api-artifacts-tasks). + This might be more complex, as the `androidComponents` API + is designed to be more stable to keep plugins compatible longer. Check our + [Gradle Recipes](https://github.com/android/gradle-recipes/tree/agp-9.0) + for examples. -- **Update third-party plugins:** Some third-party plugins might still depend on interfaces or APIs that are no longer exposed. Migrate to versions of those plugins which are compatible with AGP 9.0. +- **Update third-party plugins:** + Some third-party plugins might still depend on interfaces or APIs that are + no longer exposed. Migrate to versions of those plugins which are compatible + with AGP 9.0. -The switch to the new DSL interfaces prevents plugins and Gradle build scripts using various deprecated APIs, including: +The switch to the new DSL interfaces prevents plugins and Gradle build scripts +using various deprecated APIs, including: | Deprecated API in the `android` block | Function | Replacement | |---|---|---| @@ -54,43 +90,73 @@ The switch to the new DSL interfaces prevents plugins and Gradle build scripts u | `generatePureSplits` | Generate configuration splits for instant apps. | The ability to ship configuration splits is now built in to Android app bundles. | | `aidlPackagedList` | AIDL files to package in the AAR to expose it as API for libraries and apps that depend on this library. | This is still exposed on [`LibraryExtension`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/dsl/LibraryExtension) but not on the other extension types. | -If you update to AGP 9.0 and see the following error message, it means that your project is still referencing some of the old types: +If you update to AGP 9.0 and see the following error message, it means that +your project is still referencing some of the old types: java.lang.ClassCastException: class com.android.build.gradle.internal.dsl.ApplicationExtensionImpl$AgpDecorated_Decorated cannot be cast to class com.android.build.gradle.BaseExtension -If you are blocked by incompatible third-party plugins, you can opt out and get back the old implementations for the DSL, as well as the old variant API. While doing this, the new interfaces are also available, and you can still update your own build logic to the new API. To opt out, include this line in your `gradle.properties` file: +If you are blocked by incompatible third-party plugins, you can opt out and +get back the old implementations for the DSL, as well as the old variant API. +While doing this, the new interfaces are also available, and you can still +update your own build logic to the new API. To opt out, include this line in +your `gradle.properties` file: android.newDsl=false -Alternatively, for a more gradual migration, AGP 9.4 lets you opt out individual modules. To learn how, see [Variant API module opt-out](https://developer.android.com/build/releases/agp-9-4-0-release-notes#new-dsl-opt-out). +Alternatively, for a more gradual migration, AGP 9.4 lets you opt out individual +modules. To learn how, see [Variant API module opt-out](https://developer.android.com/build/releases/agp-9-4-0-release-notes#new-dsl-opt-out). -The previous classes are marked as deprecated in AGP 9.0. This means projects that opt out of the `newDsl` flag will see deprecation warnings, including on the `android` block itself. +The previous classes are marked as deprecated in AGP 9.0. This means projects +that opt out of the `newDsl` flag will see deprecation warnings, including on +the `android` block itself. > [!CAUTION] > **Caution:** The ability to opt-out will be removed in AGP 10.0 (mid-2026). -You can also start upgrading to the new APIs before upgrading to AGP 9.0. The new interfaces have been present for many AGP versions and so you can have a mix of new and old. The [AGP API reference docs](https://developer.android.com/reference/tools/gradle-api) show the API surface for each AGP version, and when each class, method and field was added. +You can also start upgrading to the new APIs before upgrading to AGP 9.0. The +new interfaces have been present for many AGP versions and so you can have a mix +of new and old. The [AGP API reference docs](https://developer.android.com/reference/tools/gradle-api) show the API +surface for each AGP version, and when each class, method and field was +added. -We're reaching out to the authors of commonly used plugins to help them adapt and release plugins that are fully compatible with the new modes, and will continue to enhance the AGP Upgrade Assistant in Android Studio to guide you through the migration. +We're reaching out to the authors of commonly used plugins to help them adapt +and release plugins that are fully compatible with the new modes, and will +continue to enhance the AGP Upgrade Assistant in Android Studio to guide you +through the migration. -If you find that the new DSL or Variant API are missing capabilities or features, please file an [issue](https://issuetracker.google.com/issues/new?component=192708&template=840533) as soon as possible. +If you find that the new DSL or Variant API are missing capabilities or +features, please file an [issue](https://issuetracker.google.com/issues/new?component=192708&template=840533) as soon as possible. ## Built-in Kotlin -Android Gradle plugin 9.0 introduces built-in Kotlin support and enables it by default. That means you no longer have to apply the `org.jetbrains.kotlin.android` (or `kotlin-android`) plugin in your build files to compile Kotlin source files. This simplifies the Kotlin integration with AGP, avoids the use of deprecated APIs, and improves performance in some cases. +Android Gradle plugin 9.0 introduces built-in Kotlin support and enables it +by default. That means you no longer have to apply the +`org.jetbrains.kotlin.android` (or `kotlin-android`) plugin in your build files +to compile Kotlin source files. +This simplifies the Kotlin integration with AGP, avoids the use of +deprecated APIs, and improves performance in some cases. -Therefore, when you upgrade your project to AGP 9.0, you need to also [migrate to built-in Kotlin](https://developer.android.com/build/migrate-to-built-in-kotlin) or [opt out](https://developer.android.com/build/migrate-to-built-in-kotlin#opt-out-of-built-in-kotlin). +Therefore, when you upgrade your project to AGP 9.0, you need to also +[migrate to built-in Kotlin](https://developer.android.com/build/migrate-to-built-in-kotlin) or [opt out](https://developer.android.com/build/migrate-to-built-in-kotlin#opt-out-of-built-in-kotlin). -You can also [selectively disable built-in Kotlin support](https://developer.android.com/build/migrate-to-built-in-kotlin#selectively-disable) for Gradle subprojects that don't have Kotlin sources. +You can also [selectively disable built-in Kotlin support](https://developer.android.com/build/migrate-to-built-in-kotlin#selectively-disable) for Gradle +subprojects that don't have Kotlin sources. ## Runtime dependency on Kotlin Gradle plugin -To provide [built-in Kotlin](https://developer.android.com/build/migrate-to-built-in-kotlin) support, Android Gradle plugin 9.0 now has a runtime dependency on Kotlin Gradle plugin (KGP) 2.2.10. That means you no longer have to declare a KGP version, and if you use a KGP version lower than 2.2.10, Gradle will automatically upgrade your KGP version to 2.2.10. Likewise, if you use a KSP version lower than 2.2.10-2.0.2, AGP will upgrade it to 2.2.10-2.0.2 to match the KGP version. +To provide [built-in Kotlin](https://developer.android.com/build/migrate-to-built-in-kotlin) support, Android Gradle plugin 9.0 now has a +runtime dependency on Kotlin Gradle plugin (KGP) 2.2.10. +That means you no longer have to declare a KGP version, and if you use a KGP +version lower than 2.2.10, Gradle will automatically upgrade your KGP version to +2.2.10. +Likewise, if you use a KSP version lower than 2.2.10-2.0.2, AGP will upgrade it +to 2.2.10-2.0.2 to match the KGP version. ### Upgrade to a higher KGP version -To use a higher version of KGP or KSP, add the following to your top-level build file: +To use a higher version of KGP or KSP, add the following to your top-level build +file: buildscript { dependencies { @@ -104,9 +170,13 @@ To use a higher version of KGP or KSP, add the following to your top-level build ### Downgrade to a lower KGP version -You can only downgrade the KGP version if you've [opted out of built-in Kotlin](https://developer.android.com/build/migrate-to-built-in-kotlin#opt-out-of-built-in-kotlin). This is because AGP 9.0 enables built-in Kotlin by default, and built-in Kotlin requires KGP 2.2.10 or higher. +You can only downgrade the KGP version if you've +[opted out of built-in Kotlin](https://developer.android.com/build/migrate-to-built-in-kotlin#opt-out-of-built-in-kotlin). +This is because AGP 9.0 enables built-in Kotlin by default, and built-in Kotlin +requires KGP 2.2.10 or higher. -To use a lower version of KGP or KSP, declare that version in your top-level build file using a [strict version](https://docs.gradle.org/current/userguide/dependency_versions.html#sec:strict-version) declaration: +To use a lower version of KGP or KSP, declare that version in your top-level +build file using a [strict version](https://docs.gradle.org/current/userguide/dependency_versions.html#sec:strict-version) declaration: buildscript { dependencies { @@ -129,13 +199,17 @@ Note that the minimum KGP version you can downgrade to is 2.0.0. ## IDE support for test fixtures -AGP 9.0 brings full Android Studio IDE support for [test fixtures](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/dsl/TestFixtures). +AGP 9.0 brings full Android Studio IDE support for +[test fixtures](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/dsl/TestFixtures). ## Fused Library Plugin -The Fused Library Plugin (Preview) lets you publish multiple libraries as a single Android Library AAR. This can make it easier for your users to depend on your published artifacts. +The Fused Library Plugin (Preview) lets you publish multiple +libraries as a single Android Library AAR. This can make it easier for your +users to depend on your published artifacts. -For information about getting started, see [Publish multiple Android libraries as one with Fused Library](https://developer.android.com/build/publish-library/fused-library). +For information about getting started, see +[Publish multiple Android libraries as one with Fused Library](https://developer.android.com/build/publish-library/fused-library). ## Behavior changes @@ -146,7 +220,8 @@ Android Gradle plugin 9.0 has the following new behaviors: | Android Gradle plugin 9.0 uses NDK version `r28c` by default. | Consider specifying the NDK version you want to use explicitly. | | Android Gradle plugin 9.0 by default requires consumers of a library to use the same or higher compile SDK version. | Use the same or higher compile SDK when consuming a library. If this is not possible, or you want to give consumers of a library you publish more time to switch, set [`AarMetadata.minCompileSdk`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/dsl/AarMetadata#minCompileSdk()) explicitly. | -AGP 9.0 includes updates to the following Gradle properties' defaults. This gives you the choice to preserve the AGP 8.13 behavior when upgrading: +AGP 9.0 includes updates to the following Gradle properties' defaults. +This gives you the choice to preserve the AGP 8.13 behavior when upgrading: | Property | Function | Change from AGP 8.13 to AGP 9.0 | Recommendation | |---|---|---|---| @@ -161,7 +236,7 @@ AGP 9.0 includes updates to the following Gradle properties' defaults. This give | `android.onlyEnableUnitTestForTheTestedBuildType` | Only creates unit test components for the tested build type. In the default project this results in a single unit test for debug, where the previous behavor was to have unit tests run for debug or release. | `false` → `true` | If your project doesn't require tests to run for both debug and release, no change is required. | | `android.proguard.failOnMissingFiles` | Fails the build with an error if any of the keep files specified in the AGP DSL don't exist on disk. Before this change typos in filenames would result in files being silently ignored. | `false` → `true` | Remove any invalid proguard files declarations | | `android.r8.optimizedResourceShrinking` | Allows R8 to keep fewer Android resources by considering classes and Android resources together. | `false` → `true` | If your project's keep rules are already complete, no change is required. | -| `android.r8.strictFullModeForKeepRules` | Allows R8 to keep less by not implicitly keeping the default constructor when a class is kept. That is, `-keep class A` no longer implies `-keep class A { (); }` | `false` → `true` | If your project's keep rules are already complete, no change is required. Replace `-keep class A` with `-keep class A { (); }` in your project's keep rules for any cases where you need the default constructor to be kept. | +| `android.r8.strictFullModeForKeepRules` | Allows R8 to keep less by not implicitly keeping the default constructor when a class is kept. That is, `-keep class A` no longer implies `-keep class A { (); }` | `false` → `true` | If your project's keep rules are already complete, no change is required.
Replace `-keep class A` with `-keep class A { (); }` in your project's keep rules for any cases where you need the default constructor to be kept. | | `android.defaults.buildfeatures.resvalues` | Enables [`resValues`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/variant/HasAndroidResources#resValues()) in all subprojects | `true` → `false` | Enable `resValues` in only the subprojects that need it by setting the following in those projects' Gradle build files: ``` android { buildFeatures { resValues = true } } ``` | | `android.defaults.buildfeatures.shaders` | Enables [shader compilation](https://developer.android.com/ndk/guides/graphics/shader-compilers) in all subprojects | `true` → `false` | Enable shader compilation in only the subprojects that contain shaders to be compiled by setting the following in those projects' Gradle build files: ``` android { buildFeatures { shaders = true } } ``` | | `android.r8.proguardAndroidTxt.disallowed` | In AGP 9.0, `getDefaultProguardFile()` will only support `proguard-android-optimize.txt` rather than `proguard-android.txt`. This is to prevent accidental usage of the `­dontoptimize` flag, which is included in `proguard-android.txt`. | `false` → `true` | You can explicitly specify `­dontoptimize` in a custom proguardFile if you want to avoid optimization, alongside using `proguard-android-optimize.txt`. Make sure to remove the `­dontoptimize` flag from this file if possible, as it reduces R8 optimization benefits. If not, opt out by setting `android.r8.globalOptionsInConsumerRules.disallowed=false`. | @@ -185,9 +260,16 @@ Android Gradle plugin 9.0 has the following breaking DSL changes: - The parameterization of [`CommonExtension`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/dsl/CommonExtension) has been removed. - In itself, this is only a source-level breaking change to help avoid future source-level breaking changes, but it also means that the block methods need to move from [`CommonExtension`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/dsl/CommonExtension) to [`ApplicationExtension`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/dsl/ApplicationExtension), [`LibraryExtension`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/dsl/LibraryExtension), [`DynamicFeatureExtension`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/dsl/DynamicFeatureExtension) and [`TestExtension`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/dsl/TestExtension). + In itself, this is only a source-level breaking change to help + avoid future source-level breaking changes, but it also means that + the block methods need to move from [`CommonExtension`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/dsl/CommonExtension) to + [`ApplicationExtension`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/dsl/ApplicationExtension), [`LibraryExtension`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/dsl/LibraryExtension), + [`DynamicFeatureExtension`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/dsl/DynamicFeatureExtension) and [`TestExtension`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/dsl/TestExtension). - When upgrading your project to AGP 9.0, refactor Gradle plugin code which uses those parameters or the block methods. For example the following plugin is updated to remove the type parameter and not rely on the removed block methods: + When upgrading your project to AGP 9.0, refactor Gradle plugin code + which uses those parameters or the block methods. For example the following + plugin is updated to remove the type parameter and not rely on the + removed block methods: **AGP 8.13** @@ -213,7 +295,8 @@ Android Gradle plugin 9.0 has the following breaking DSL changes: } } - For plugins which target a range of AGP versions, using the getter directly is binary compatible with AGP versions lower than 9.0. + For plugins which target a range of AGP versions, using the getter directly + is binary compatible with AGP versions lower than 9.0. ## Removed DSL @@ -221,37 +304,50 @@ Android Gradle plugin 9.0 removes: - [`AndroidSourceSet.jni`](https://developer.android.com/reference/tools/gradle-api/8.13/com/android/build/api/dsl/AndroidSourceSet#jni(kotlin.Function1)), because it was not functional. -- [`AndroidSourceSet.wearAppConfigurationName`](https://developer.android.com/reference/tools/gradle-api/8.13/com/android/build/api/dsl/AndroidSourceSet#wearAppConfigurationName()), as it relates to the removed embedded Wear OS app support. +- [`AndroidSourceSet.wearAppConfigurationName`](https://developer.android.com/reference/tools/gradle-api/8.13/com/android/build/api/dsl/AndroidSourceSet#wearAppConfigurationName()), as it relates to the + removed embedded Wear OS app support. - [`BuildType.isRenderscriptDebuggable`](https://developer.android.com/reference/tools/gradle-api/8.13/com/android/build/api/dsl/BuildType#isRenderscriptDebuggable()), because it was not functional. -- [`DependencyVariantSelection`](https://developer.android.com/reference/tools/gradle-api/8.13/com/android/build/api/dsl/DependencyVariantSelection). It is replaced By [`DependencySelection`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/dsl/DependencySelection), which is exposed as [`kotlin.android.localDependencySelection`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/dsl/KotlinMultiplatformAndroidLibraryExtension#localDependencySelection(kotlin.Function1)) +- [`DependencyVariantSelection`](https://developer.android.com/reference/tools/gradle-api/8.13/com/android/build/api/dsl/DependencyVariantSelection). It is replaced By + [`DependencySelection`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/dsl/DependencySelection), which is exposed as + [`kotlin.android.localDependencySelection`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/dsl/KotlinMultiplatformAndroidLibraryExtension#localDependencySelection(kotlin.Function1)) -- [`Installation.installOptions(String)`](https://developer.android.com/reference/tools/gradle-api/8.13/com/android/build/api/dsl/Installation#installOptions(kotlin.String)). It is replaced by the mutable property of [`Installation.installOptions`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/dsl/Installation#installOptions()). +- [`Installation.installOptions(String)`](https://developer.android.com/reference/tools/gradle-api/8.13/com/android/build/api/dsl/Installation#installOptions(kotlin.String)). It is replaced by the + mutable property of [`Installation.installOptions`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/dsl/Installation#installOptions()). - The experimental, but never stabilized [`PostProcessing`](https://developer.android.com/reference/tools/gradle-api/8.13/com/android/build/api/dsl/PostProcessing) block. -- [`ProductFlavor.setDimension`](https://developer.android.com/reference/tools/gradle-api/8.13/com/android/build/api/dsl/ProductFlavor#setDimension(kotlin.String)), which is replaced by the [`dimension`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/dsl/ProductFlavor#dimension()) property +- [`ProductFlavor.setDimension`](https://developer.android.com/reference/tools/gradle-api/8.13/com/android/build/api/dsl/ProductFlavor#setDimension(kotlin.String)), which is replaced by the + [`dimension`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/dsl/ProductFlavor#dimension()) property -- `LanguageSplitOptions`, which was only useful for [Google Play Instant](https://developer.android.com/topic/google-play-instant), which is deprecated. +- `LanguageSplitOptions`, which was only useful for + [Google Play Instant](https://developer.android.com/topic/google-play-instant), which is deprecated. -- [`DensitySplit`](https://developer.android.com/reference/tools/gradle-api/8.13/com/android/build/api/dsl/DensitySplit), because the feature is no longer supported. Replacement is to use [App Bundles](https://developer.android.com/guide/app-bundle). +- [`DensitySplit`](https://developer.android.com/reference/tools/gradle-api/8.13/com/android/build/api/dsl/DensitySplit), because the feature is no longer supported. + Replacement is to use [App Bundles](https://developer.android.com/guide/app-bundle). ## Removed APIs Android Gradle plugin 9.0 removes: -- [`AndroidComponentsExtension.finalizeDSl`](https://developer.android.com/reference/tools/gradle-api/8.13/com/android/build/api/variant/AndroidComponentsExtension#finalizeDSl(org.gradle.api.Action)). It is replaced by [`finalizeDsl`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/variant/DslLifecycle#finalizeDsl(kotlin.Function1)) +- [`AndroidComponentsExtension.finalizeDSl`](https://developer.android.com/reference/tools/gradle-api/8.13/com/android/build/api/variant/AndroidComponentsExtension#finalizeDSl(org.gradle.api.Action)). It is replaced by + [`finalizeDsl`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/variant/DslLifecycle#finalizeDsl(kotlin.Function1)) -- [`Component.transformClassesWith`](https://developer.android.com/reference/tools/gradle-api/8.13/com/android/build/api/variant/Component#transformClassesWith(java.lang.Class,com.android.build.api.instrumentation.InstrumentationScope,kotlin.Function1)). It is replaced by [`Instrumentation.transformClassesWith`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/variant/Instrumentation#transformClassesWith(java.lang.Class,com.android.build.api.instrumentation.InstrumentationScope,kotlin.Function1)) +- [`Component.transformClassesWith`](https://developer.android.com/reference/tools/gradle-api/8.13/com/android/build/api/variant/Component#transformClassesWith(java.lang.Class,com.android.build.api.instrumentation.InstrumentationScope,kotlin.Function1)). It is replaced by + [`Instrumentation.transformClassesWith`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/variant/Instrumentation#transformClassesWith(java.lang.Class,com.android.build.api.instrumentation.InstrumentationScope,kotlin.Function1)) -- [`Component.setAsmFramesComputationMode`](https://developer.android.com/reference/tools/gradle-api/8.13/com/android/build/api/variant/Component#setAsmFramesComputationMode(com.android.build.api.instrumentation.FramesComputationMode)). It is replaced by [`Instrumentation.setAsmFramesComputationMode`](https://developer.android.com/reference/tools/gradle-api/8.13/com/android/build/api/variant/Instrumentation#setAsmFramesComputationMode(com.android.build.api.instrumentation.FramesComputationMode)) +- [`Component.setAsmFramesComputationMode`](https://developer.android.com/reference/tools/gradle-api/8.13/com/android/build/api/variant/Component#setAsmFramesComputationMode(com.android.build.api.instrumentation.FramesComputationMode)). It is replaced by + [`Instrumentation.setAsmFramesComputationMode`](https://developer.android.com/reference/tools/gradle-api/8.13/com/android/build/api/variant/Instrumentation#setAsmFramesComputationMode(com.android.build.api.instrumentation.FramesComputationMode)) -- [`ComponentBuilder.enabled`](https://developer.android.com/reference/tools/gradle-api/8.13/com/android/build/api/variant/ComponentBuilder#enabled()). It is replaced by [`ComponentBuilder.enable`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/variant/ComponentBuilder#enable()). +- [`ComponentBuilder.enabled`](https://developer.android.com/reference/tools/gradle-api/8.13/com/android/build/api/variant/ComponentBuilder#enabled()). It is replaced by + [`ComponentBuilder.enable`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/variant/ComponentBuilder#enable()). -- [`DependenciesInfoBuilder.includedInApk`](https://developer.android.com/reference/tools/gradle-api/8.13/com/android/build/api/variant/DependenciesInfoBuilder#includedInApk()). Is is replaced by [`includeInApk`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/variant/DependenciesInfoBuilder#includeInApk()) +- [`DependenciesInfoBuilder.includedInApk`](https://developer.android.com/reference/tools/gradle-api/8.13/com/android/build/api/variant/DependenciesInfoBuilder#includedInApk()). Is is replaced by + [`includeInApk`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/variant/DependenciesInfoBuilder#includeInApk()) -- [`DependenciesInfoBuilder.includedInBundle`](https://developer.android.com/reference/tools/gradle-api/8.13/com/android/build/api/variant/DependenciesInfoBuilder#includedInBundle()). Is is replaced by [`includeInBundle`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/variant/DependenciesInfoBuilder#includeInBundle()) +- [`DependenciesInfoBuilder.includedInBundle`](https://developer.android.com/reference/tools/gradle-api/8.13/com/android/build/api/variant/DependenciesInfoBuilder#includedInBundle()). Is is replaced by + [`includeInBundle`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/variant/DependenciesInfoBuilder#includeInBundle()) - [`GeneratesApk.targetSdkVersion`](https://developer.android.com/reference/tools/gradle-api/8.13/com/android/build/api/variant/GeneratesApk#targetSdkVersion()). Is is replaced by [`targetSdk`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/variant/GeneratesApk#targetSdk()) @@ -261,25 +357,43 @@ Android Gradle plugin 9.0 removes: - [`Variant.targetSdkVersion`](https://developer.android.com/reference/tools/gradle-api/8.13/com/android/build/api/variant/Variant#targetSdkVersion()). Is is replaced by [`targetSdk`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/variant/Variant#targetSdk()) -- [`Variant.unitTest`](https://developer.android.com/reference/tools/gradle-api/8.13/com/android/build/api/variant/Variant#unitTest()), as it was not applicable to the [`com.android.test`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/gradle/TestPlugin) plugin. `unitTest` is available on [`VariantBuilder`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/variant/VariantBuilder) subtypes extending [`HasUnitTest`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/variant/HasUnitTest). +- [`Variant.unitTest`](https://developer.android.com/reference/tools/gradle-api/8.13/com/android/build/api/variant/Variant#unitTest()), as it was not applicable to the + [`com.android.test`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/gradle/TestPlugin) plugin. + `unitTest` is available on [`VariantBuilder`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/variant/VariantBuilder) subtypes extending + [`HasUnitTest`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/variant/HasUnitTest). -- [`VariantBuilder.targetSdk`](https://developer.android.com/reference/tools/gradle-api/8.13/com/android/build/api/variant/VariantBuilder#targetSdk()) and [`targetSdkPreview`](https://developer.android.com/reference/tools/gradle-api/8.13/com/android/build/api/variant/VariantBuilder#targetSdkPreview()), as they were not meaningful in libraries. Use [`GeneratesApkBuilder.targetSdk`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/variant/GeneratesApkBuilder#targetSdk()) or [`GeneratesApkBuilder.targetSdkPreview`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/variant/GeneratesApkBuilder#targetSdkPreview()) instead. +- [`VariantBuilder.targetSdk`](https://developer.android.com/reference/tools/gradle-api/8.13/com/android/build/api/variant/VariantBuilder#targetSdk()) and [`targetSdkPreview`](https://developer.android.com/reference/tools/gradle-api/8.13/com/android/build/api/variant/VariantBuilder#targetSdkPreview()), + as they were not meaningful in libraries. Use + [`GeneratesApkBuilder.targetSdk`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/variant/GeneratesApkBuilder#targetSdk()) or + [`GeneratesApkBuilder.targetSdkPreview`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/variant/GeneratesApkBuilder#targetSdkPreview()) + instead. -- [`VariantBuilder.enableUnitTest`](https://developer.android.com/reference/tools/gradle-api/8.13/com/android/build/api/variant/VariantBuilder#enableUnitTest()), as it was not applicable to the [`com.android.test`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/gradle/TestPlugin) plugin. `enableUnitTest` is available on [`VariantBuilder`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/variant/VariantBuilder) subtypes extending [`HasUnitTestBuilder`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/variant/HasUnitTestBuilder). +- [`VariantBuilder.enableUnitTest`](https://developer.android.com/reference/tools/gradle-api/8.13/com/android/build/api/variant/VariantBuilder#enableUnitTest()), as it was not applicable to the + [`com.android.test`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/gradle/TestPlugin) plugin. + `enableUnitTest` is available on [`VariantBuilder`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/variant/VariantBuilder) subtypes extending + [`HasUnitTestBuilder`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/variant/HasUnitTestBuilder). -- [`VariantBuilder.unitTestEnabled`](https://developer.android.com/reference/tools/gradle-api/8.13/com/android/build/api/variant/VariantBuilder#unitTestEnabled()) is removed in favor of the more consistently named [`enableUnitTest`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/variant/HasUnitTestBuilder#enableUnitTest()) on the [`VariantBuilder`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/variant/VariantBuilder) subtypes extending [`HasUnitTestBuilder`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/variant/HasUnitTestBuilder). +- [`VariantBuilder.unitTestEnabled`](https://developer.android.com/reference/tools/gradle-api/8.13/com/android/build/api/variant/VariantBuilder#unitTestEnabled()) is removed in favor of the more + consistently + named [`enableUnitTest`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/variant/HasUnitTestBuilder#enableUnitTest()) on the [`VariantBuilder`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/variant/VariantBuilder) subtypes + extending [`HasUnitTestBuilder`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/variant/HasUnitTestBuilder). - [`VariantOutput.enable`](https://developer.android.com/reference/tools/gradle-api/8.13/com/android/build/api/variant/VariantOutput#enable()). Is is replaced by [`enabled`](https://developer.android.com/reference/tools/gradle-api/9.0/com/android/build/api/variant/VariantOutput#enabled()) - The deprecated and disabled `FeaturePlugin` and `FeatureExtension`. -- The deprecated and disabled `BaseExtension.registerTransform` APIs, which only remained to allow compiling against the latest AGP version while targeting running on AGP 4.2 or lower. +- The deprecated and disabled `BaseExtension.registerTransform` APIs, which + only remained to allow compiling against the latest AGP version while + targeting running on AGP 4.2 or lower. ## Removed Gradle properties -The following Gradle properties were initially added as ways to globally disable features that were enabled by default. +The following Gradle properties were initially added as ways to globally +disable features that were enabled by default. -These features have been disabled by default since AGP 8.0 or lower. Enable these features in only the sub-projects that use them for a more efficient build. +These features have been disabled by default since AGP 8.0 or lower. Enable +these features in only the sub-projects that use them for a more efficient +build. | Property | Function | Replacement | |---|---|---| @@ -290,7 +404,8 @@ These features have been disabled by default since AGP 8.0 or lower. Enable thes AGP 9.0 throws an error if you set the following Gradle properties. -The [Android Gradle plugin Upgrade Assistant](https://developer.android.com/build/agp-upgrade-assistant) won't upgrade projects to AGP 9.0 that use these properties. +The [Android Gradle plugin Upgrade Assistant](https://developer.android.com/build/agp-upgrade-assistant) won't upgrade projects to +AGP 9.0 that use these properties. | Property | Function | |---|---| @@ -303,7 +418,11 @@ The following R8 changes are included in AGP 9.0.0. ### New configuration option `-processkotlinnullchecks` -We've added the new R8 option `-processkotlinnullchecks` to configure R8 for processing Kotlin null checks. The option takes a mandatory argument that must be one of the following three values: `keep`, `remove_message` and `remove`. The option processes the following null checks added by the Kotlin compiler: +We've added the new R8 option `-processkotlinnullchecks` to configure R8 for +processing Kotlin null checks. The option takes a mandatory argument that must +be one of the following three values: `keep`, `remove_message` and `remove`. +The option processes the following null checks added by the Kotlin +compiler: class kotlin.jvm.internal.Intrinsics { void checkNotNull(java.lang.Object); @@ -323,68 +442,108 @@ We've added the new R8 option `-processkotlinnullchecks` to configure R8 for pro void checkNotNullParameter(java.lang.Object, java.lang.String); } -The option values, ordered from the weakest to the strongest, have the following effect: +The option values, ordered from the weakest to the strongest, have the +following effect: - `keep` doesn't change the checks. - `remove_message` rewrites each check method call to a call to `getClass()` on the first argument of the call (effectively keeping the null check, but without any message). - `remove` completely removes the checks. -By default R8 uses `remove_message`. Any specification of `-processkotlinnullchecks` will override that. If specified multiple times the strongest value is used. +By default R8 +uses `remove_message`. Any specification of `-processkotlinnullchecks` +will override that. If specified multiple times the strongest value is +used. ### Stop propagating keep info to companion methods -When keep rules match interface methods that are subject to desugaring, R8 previously internally transferred the *disallow optimization* and *disallow shrinking* bits to the synthesized companion methods. +When keep rules match interface methods that are subject to desugaring, +R8 previously internally transferred the *disallow optimization* and *disallow +shrinking* bits to the synthesized companion methods. -Starting with AGP 9.0, keep rules no longer apply to companion methods. This is consistent with the fact that keep rules are not applicable to other compiler synthesized fields/methods/classes. +Starting with AGP 9.0, keep rules no longer apply to +companion methods. This is consistent with the fact that keep rules are +not applicable to other compiler synthesized fields/methods/classes. -By transferring the *disallow optimization* and *disallow shrinking* bits to the companion methods, the following use case was previously supported: +By transferring the *disallow optimization* and *disallow shrinking* bits to +the companion methods, the following use case was previously supported: 1. Compile a library with `default`/`static`/`private` interface methods to DEX with `minSdk` \< 24 and rules that keep the interface methods. 2. Compile an app with the library on classpath and `-applymapping`. 3. Merge the app and the library. -Note that this only works with `-applymapping` since the `disallow obfuscation` bit is not transferred to the companion methods---that is, the companion classes generated from step 1 would have obfuscated method names. +Note that this only works with `-applymapping` since the `disallow +obfuscation` bit is not transferred to the companion methods---that is, the +companion classes generated from step 1 would have obfuscated method +names. -Going forward this use case is no longer supported for `minSdk` \< 24. The workaround is to do the following: +Going forward this use case is no longer supported for `minSdk` \< +24. The workaround is to do the following: 1. Desugar the library with `default`/`static`/`private` interface methods to class files with `minSdk` \< 24. 2. Compile the desugared artifact using R8 and rules that keep the interface methods on the companion classes. 3. Compile the app with the library on classpath. 4. Merge the app and the desugared artifact. -Another side effect of this is that it is no longer possible to keep the inner class and enclosing method attributes for anonymous and local classes inside interface companion methods. +Another side effect of this is that it is no longer possible to keep the +inner class and enclosing method attributes for anonymous and local classes +inside interface companion methods. ### Change the default emitted source file to `r8-map-id-` This change is in AGP starting from 8.12.0. -The default emitted source file attribute for a class changes from `SourceFile` to `r8-map-id-` when retracing is required (that is, when either obfuscation or optimization is enabled). +The default emitted source file attribute for a class changes from +`SourceFile` to `r8-map-id-` when retracing is required (that is, +when either obfuscation or optimization is enabled). -Given an obfuscated stack trace, the new source file attribute makes it possible to extract the ID of the mapping file that is required for retracing, which can be used to support [automated retracing of stack traces in Logcat](https://developer.android.com/studio/preview/features#logcat-retrace). +Given an obfuscated stack trace, the new source file attribute makes it +possible to extract the ID of the mapping file that is required for +retracing, which can be used to support +[automated retracing of stack traces in Logcat](https://developer.android.com/studio/preview/features#logcat-retrace). -If a custom source file attribute is used (`-renamesourcefileattribute`) this custom source file attribute continues to take precedence. +If a custom source file attribute is used (`-renamesourcefileattribute`) +this custom source file attribute continues to take precedence. -In ProGuard compatibility mode (when `gradle.properties` contains `android.enableR8.fullMode=false`), emitting a source file attribute of `r8-map-id-` only takes effect if the `SourceFile` attribute is *not* kept. Apps that use ProGuard compatibility mode and want to include the mapping file ID in their stack traces should remove `-keepattributes SourceFile` (or migrate to R8 full mode). +In ProGuard compatibility mode (when `gradle.properties` contains +`android.enableR8.fullMode=false`), emitting a source file attribute of +`r8-map-id-` only takes effect if the `SourceFile` +attribute is *not* kept. Apps that use ProGuard compatibility mode and want +to include the mapping file ID in their stack traces should remove +`-keepattributes SourceFile` (or migrate to R8 full mode). -The map ID used in `r8-map-id-` is the full map hash, and not a 7 character prefix of the map hash which was previously used. +The map ID used in `r8-map-id-` is the full map hash, and not a 7 +character prefix of the map hash which was previously used. ### Enable use of minimized synthetic names in L8 desugaring -The name of synthetic classes generated by D8 normally contains the substring `$$ExternalSynthetic` that tells you that this is a synthetic generated by D8. Moreover, the name of the synthetic also encodes the synthetic kind (for example, `Backport`, `Lambda`). This has a negative impact on the resulting DEX size, since the class names take up more space in the string pool. +The name of synthetic classes generated by D8 normally contains the substring +`$$ExternalSynthetic` that tells you that this is a synthetic generated by D8. +Moreover, the name of the synthetic also encodes the synthetic kind (for +example, `Backport`, `Lambda`). This has a negative impact on the resulting DEX +size, since the class names take up more space in the string pool. -AGP 9.0 configures L8 (core library desugaring) so that the DEX file containing all `j$` classes uses a new shortened class name format for synthetic classes. The new class name uses a numeric ID (for example, `$1`). +AGP 9.0 configures L8 (core library desugaring) so that the DEX +file containing all `j$` classes uses a new shortened class name format +for synthetic classes. The new class name uses a numeric ID (for example, `$1`). ### Remove support for `-addconfigurationdebugging` -AGP 9.0 removes support for `-addconfigurationdebugging`. The compiler now reports a warning if the flag is used. +AGP 9.0 removes support for `-addconfigurationdebugging`. The compiler now +reports a warning if the flag is used. ### Remove support for generating L8 rules from D8/R8 -This change is only relevant for developers using the D8/R8 command line or APIs directly. +This change is only relevant for developers using the D8/R8 command line or +APIs directly. -R8 9.0 removes support for generating keep rules for L8 from D8 and R8. You should instead use `TraceReferences` for this purpose. +R8 9.0 removes support for generating keep rules for L8 from D8 and R8. +You should instead use `TraceReferences` for this purpose. -More specifically, the methods `D8Command.builder.setDesugaredLibraryKeepRuleConsumer` and `R8Command.Builder.setDesugaredLibraryKeepRuleConsumer` are removed, and the support for `--desugared-lib-pg-conf-output` is removed from the command line options of D8 and R8. +More specifically, the methods +`D8Command.builder.setDesugaredLibraryKeepRuleConsumer` and +`R8Command.Builder.setDesugaredLibraryKeepRuleConsumer` are removed, and the +support for `--desugared-lib-pg-conf-output` is removed from the command line +options of D8 and R8. ## Fixed issues diff --git a/build-system/agp/agp-9-upgrade/references/buildconfig.md b/build-system/agp/agp-9-upgrade/references/buildconfig.md index 773fcd8..e4e19b6 100644 --- a/build-system/agp/agp-9-upgrade/references/buildconfig.md +++ b/build-system/agp/agp-9-upgrade/references/buildconfig.md @@ -1,4 +1,5 @@ -When an Android module contains custom BuildConfig fields, the following steps are necessary to ensure a correct build. +When an Android module contains custom BuildConfig fields, the following steps +are necessary to ensure a correct build. ### Step 1: Enable the buildConfig build feature @@ -36,9 +37,11 @@ In custom build-logic using `CommonExtension`: ### Step 2: Migrate to the new API -Use the **addCustomBuildConfigFields** recipe from the [gradle-recipes](https://developer.android.com/agents/skills/build-system/agp/agp-9-upgrade/references/recipes) repository. +Use the **addCustomBuildConfigFields** recipe from the [gradle-recipes](https://developer.android.com/agents/skills/build-system/agp/agp-9-upgrade/references/recipes) +repository. -**IMPORTANT:** For `BuildConfigField`s with a type of `String`, the `value` field *must* include quotation marks as part of the String. For example: +**IMPORTANT:** For `BuildConfigField`s with a type of `String`, the `value` field +*must* include quotation marks as part of the String. For example: BuildConfigField( type = "String", @@ -46,4 +49,6 @@ Use the **addCustomBuildConfigFields** recipe from the [gradle-recipes](https:// comment = "Optional comment", ) -It is an **error** if the `value` field doesn't include quotation marks as part of the String. For example, `value = "Some value"` **is an error** . This is because the `value` is written out literally. \ No newline at end of file +It is an **error** if the `value` field doesn't include quotation marks as +part of the String. For example, `value = "Some value"` **is an error** . This is +because the `value` is written out literally. \ No newline at end of file diff --git a/build-system/agp/agp-9-upgrade/references/ksp-kapt.md b/build-system/agp/agp-9-upgrade/references/ksp-kapt.md index 2dacb96..e8a5d33 100644 --- a/build-system/agp/agp-9-upgrade/references/ksp-kapt.md +++ b/build-system/agp/agp-9-upgrade/references/ksp-kapt.md @@ -1,23 +1,39 @@ -When migrating to built-in Kotlin, it is important to consider usage of `kapt` and the `org.jetbrains.kotlin.kapt` (also known as the `kotlin("kapt")`) plugin. The goal is to migrate as many `kapt` usages to `ksp` as possible. +When migrating to built-in Kotlin, it is important to consider usage of `kapt` +and the `org.jetbrains.kotlin.kapt` (also known as the `kotlin("kapt")`) plugin. +The goal is to migrate as many `kapt` usages to `ksp` as possible. Follow these steps when migrating `kapt`: ## 1. Remove all references to the `org.jetbrains.kotlin.kapt` plugin -The `org.jetbrains.kotlin.kapt` (also known as `kotlin("kapt")`) plugin is incompatible with built-in Kotlin. Remove it when migrating to built-in Kotlin. +The `org.jetbrains.kotlin.kapt` (also known as `kotlin("kapt")`) plugin is +incompatible with built-in Kotlin. Remove it when migrating to built-in Kotlin. ## 2. Check each usage of `kapt` -Check each usage of `kapt` to see if it is compatible with `ksp`. To check if a dependency is compatible with `ksp`, inspect the dependency's jar. For it to be compatible with `ksp`, the jar must have a file, `services/com.google.devtools.ksp.processing.SymbolProcessorProvider`. If it does not, it is **incompatible** with `ksp`. +Check each usage of `kapt` to see if it is compatible with `ksp`. To check if a +dependency is compatible with `ksp`, inspect the dependency's jar. For it to be +compatible with `ksp`, the jar must have a file, +`services/com.google.devtools.ksp.processing.SymbolProcessorProvider`. If it +does not, it is **incompatible** with `ksp`. -For example, the `androidx.room:room-compiler` library is compatible with KSP since version 2.3.0-beta02. We can verify this by finding the jar file in the Gradle caches directory, which is typically located at `~/.gradle/caches/modules-2/files-2.1/` on Linux and Mac. In this specific case, the `androidx.room:room-compiler` dependency is located at `~/.gradle/caches/modules-2/files-2.1/androidx.room/room-compiler/`. +For example, the `androidx.room:room-compiler` library is compatible with KSP +since version 2.3.0-beta02. We can verify this by finding the jar file in the +Gradle caches directory, which is typically located at +`~/.gradle/caches/modules-2/files-2.1/` on Linux and Mac. In this specific case, +the `androidx.room:room-compiler` dependency is located at +`~/.gradle/caches/modules-2/files-2.1/androidx.room/room-compiler/`. -More generally, you can find a dependency by looking in `~/.gradle/caches/modules-2/files-2.1/group-name/artifact-name/`. +More generally, you can find a dependency by looking in +`~/.gradle/caches/modules-2/files-2.1/group-name/artifact-name/`. ## 3. Migrate to KSP where possible -For each usage of `kapt` that is compatible with `ksp`, use `ksp`. The prior step explains how to check compatibility. +For each usage of `kapt` that is compatible with `ksp`, use `ksp`. The prior +step explains how to check compatibility. ## 4. Apply legacy-kapt -If a Gradle module has `kapt` dependencies that cannot be migrated to `ksp` because they are incompatible (see step 2), then leave that dependency alone and apply the `com.android.legacy-kapt` plugin. \ No newline at end of file +If a Gradle module has `kapt` dependencies that cannot be migrated to `ksp` +because they are incompatible (see step 2), then leave that dependency alone and +apply the `com.android.legacy-kapt` plugin. \ No newline at end of file diff --git a/build-system/agp/agp-9-upgrade/references/paparazzi-gradle-9.md b/build-system/agp/agp-9-upgrade/references/paparazzi-gradle-9.md index 97a65bc..a20da2a 100644 --- a/build-system/agp/agp-9-upgrade/references/paparazzi-gradle-9.md +++ b/build-system/agp/agp-9-upgrade/references/paparazzi-gradle-9.md @@ -1,10 +1,19 @@ -If Paparazzi is used in the project, update it to version 2.0.0-alpha04 or higher. +If Paparazzi is used in the project, update it to version 2.0.0-alpha04 or +higher. -Paparazzi version 2.0.0-alpha04 and lower is not fully compatible with Gradle 9, and Gradle 9 is required by AGP 9. This means that, without workarounds, projects that use Paparazzi v2.0.0-alpha04 and lower cannot migrate to AGP 9. +Paparazzi version 2.0.0-alpha04 and lower is not fully compatible with Gradle +9, and Gradle 9 is required by AGP 9. This means that, without workarounds, +projects that use Paparazzi v2.0.0-alpha04 and lower cannot migrate to AGP 9. -At time of writing, there are no higher versions of Paparazzi. That is, v2.0.0-alpha04 is the latest release. +At time of writing, there are no higher versions of Paparazzi. That is, +v2.0.0-alpha04 is the latest release. -The issue is due to Paparazzi using internal classes from Gradle that tend to move in breaking ways without warning. This specific issue is related to HTML test reports. To work around it, disable those HTML test reports. Here are two examples of how to do this, one for Kotlin DSL and the other for Groovy DSL. Any module that has the paparazzi plugin (`app.cash.paparazzi`) applied must apply one of these two workarounds. +The issue is due to Paparazzi using internal classes from Gradle that tend to +move in breaking ways without warning. This specific issue is related to HTML +test reports. To work around it, disable those HTML test reports. Here +are two examples of how to do this, one for Kotlin DSL and the other for Groovy +DSL. Any module that has the paparazzi plugin (`app.cash.paparazzi`) applied +must apply one of these two workarounds. Kotlin DSL: diff --git a/build-system/agp/agp-9-upgrade/references/recipes.md b/build-system/agp/agp-9-upgrade/references/recipes.md index a5d9856..d3ecc74 100644 --- a/build-system/agp/agp-9-upgrade/references/recipes.md +++ b/build-system/agp/agp-9-upgrade/references/recipes.md @@ -1,4 +1,6 @@ -When migrating to AGP's new DSL, any Gradle code (plugins or logic in build scripts) that relied on the old DSL will stop working. Such code must be migrated. +When migrating to AGP's new DSL, any Gradle code (plugins or logic in build +scripts) that relied on the old DSL will stop working. Such code must be +migrated. ## Guidelines @@ -6,9 +8,14 @@ When migrating to AGP's new DSL, any Gradle code (plugins or logic in build scri - **DO NOT** use AGP internals in migrated code. - **DO** use only public APIs in migrated code. -In some cases, there is a one-to-one replacement for the old code. Some examples are in [the AGP 9.0.0 release notes](https://developer.android.com/build/releases/agp-9-0-0-release-notes). +In some cases, there is a one-to-one replacement for the old code. Some examples +are in [the AGP 9.0.0 release notes](https://developer.android.com/build/releases/agp-9-0-0-release-notes). -In other cases, there is no direct one-to-one replacement. For these situations, the [gradle-recipes repo](https://github.com/android/gradle-recipes) is a great resource. You can checkout one of its AGP 9.x branches, such as `agp-9.0`, `agp-9.1`, or `agp-9.2`. These branches contain recipes for common situations in Android projects. The following table lists the compatibility for recipes for each version of AGP. +In other cases, there is no direct one-to-one replacement. For these situations, +the [gradle-recipes repo](https://github.com/android/gradle-recipes) is a great resource. You can checkout one of its +AGP 9.x branches, such as `agp-9.0`, `agp-9.1`, or `agp-9.2`. These branches +contain recipes for common situations in Android projects. The following table +lists the compatibility for recipes for each version of AGP. ## Compatibility table @@ -49,4 +56,7 @@ In the old DSL, an APK could be renamed very simply. Here's an example: } } -However, with AGP 9 and the new DSL, `applicationVariants` is no longer available. You must instead react to artifact creation using the `androidComponents.onVariants` API. A complete example of this is available in the **gradle-recipes** repository in the `listenToArtifacts` recipe. \ No newline at end of file +However, with AGP 9 and the new DSL, `applicationVariants` is no longer +available. You must instead react to artifact creation using the +`androidComponents.onVariants` API. A complete example of this is available in +the **gradle-recipes** repository in the `listenToArtifacts` recipe. \ No newline at end of file diff --git a/camera/camerax/SKILL.md b/camera/camerax/SKILL.md index b9ec31a..5ca0996 100644 --- a/camera/camerax/SKILL.md +++ b/camera/camerax/SKILL.md @@ -7,7 +7,7 @@ description: Provide technical guidance for Android camera development with Came license: Complete terms in LICENSE.txt metadata: author: Google LLC - last-updated: '2026-08-06' + last-updated: '2026-08-14' keywords: - recipe - Android @@ -26,17 +26,20 @@ metadata: - ImageAnalysis. --- -This skill provides procedural guidance and standard patterns for building camera applications on Android, with a focus on CameraX, including its `Camera2Interop` utilities, and Media3 integrations. +This skill provides procedural guidance and standard patterns for building +camera applications on Android, with a focus on CameraX, including its +`Camera2Interop` utilities, and Media3 integrations. ## Core workflows ### Handling immutable API patterns -Various Android camera and media APIs, especially CameraX `VideoCapture`, use a **fluent, immutable builder-like pattern** where methods return a new instance. Failing to reassign these results in settings, such as audio, being ignored. +Various Android camera and media APIs, especially CameraX `VideoCapture`, use a +**fluent, immutable builder-like pattern** where methods return a new instance. +Failing to reassign these results in settings, such as audio, being ignored. **Pattern: Reassignment is required** -
```kotlin // WRONG @@ -59,7 +62,6 @@ run { pending = pending.withAudioEnabled() // Reassignment val active = pending.start(exec, listener) } - ```
@@ -75,7 +77,9 @@ When migrating legacy camera codebases to the CameraX Jetpack library: ### Comprehensive feature blueprinting -For multi-step features that involve multiple files and hardware-level wiring, follow the [Structural Blueprinting](references/expert-blueprints.md) approach to avoid system timeouts. Such complex features include: +For multi-step features that involve multiple files and hardware-level wiring, +follow the [Structural Blueprinting](references/expert-blueprints.md) approach to avoid +system timeouts. Such complex features include: - **Manual controls** : Break down into the `ViewModel` state, the controller layer, and the `Camera2Interop` wiring in the session. - **RAW capture**: Separate JPEG and RAW output configurations into discrete build steps. @@ -102,16 +106,20 @@ See [modern-apis](references/modern-apis.md) for current recommendations. ### Code quality and architectural rules -Adhere to the following Android ecosystem standard patterns when building your camera implementations: +Adhere to the following Android ecosystem standard patterns when building your +camera implementations: - **Testing, fakes over mocks** : Avoid mocking libraries like `Mockito`, especially for multi-step CameraX interfaces like `ImageProxy`. Build "Fakes" to verify state rather than unreliable implementation details. - **Google Truth assertions** : Use `assertThat` over standard `JUnit` assertions like `assertEquals` for improved readability. - **Explicit test runners** : Always define an explicit `@RunWith` for test classes to ensure the CI environment executes them correctly. -- **Semantic UI merging** : When building custom camera controls in Compose, such as a button with an `Icon` and `Text`, use `semantics { mergeDescendants = true }` to ensure screen readers announce them as a single, coherent unit. +- **Semantic UI merging** : When building custom camera controls in Compose, such as a button with an `Icon` and `Text`, use `semantics { + mergeDescendants = true }` to ensure screen readers announce them as a single, coherent unit. ## Hardware and device diversity -Camera apps run on a wide variety of hardware, from mobile phones and foldables to tablets, laptops, and even smart appliances. Have consideration for the specific hardware the app is running on. +Camera apps run on a wide variety of hardware, from mobile phones and +foldables to tablets, laptops, and even smart appliances. Have consideration +for the specific hardware the app is running on. - **Form factors**: Account for screen size and orientation changes on foldables and tablets. - **Multi-camera arrays**: Some devices have a rear-facing camera and a front-facing camera. Other devices have multiple rear-facing cameras, such as wide-angle and telephoto lenses. diff --git a/camera/camerax/references/camera1-to-camerax.md b/camera/camerax/references/camera1-to-camerax.md index 03bfee5..9c6e80e 100644 --- a/camera/camerax/references/camera1-to-camerax.md +++ b/camera/camerax/references/camera1-to-camerax.md @@ -7,9 +7,9 @@ ## Initialize `ProcessCameraProvider` -Request the `ProcessCameraProvider` and bind use cases to the Activity or Fragment lifecycle. +Request the `ProcessCameraProvider` and bind use cases to the Activity or +Fragment lifecycle. -
```kotlin val context = LocalContext.current @@ -36,7 +36,6 @@ LaunchedEffect(context, lifecycleOwner) { ) val cameraControl = camera.cameraControl } - ```
@@ -51,25 +50,21 @@ Use `androidx.camera.view.PreviewView`. 1. **Set up preview**: -
```kotlin preview.setSurfaceProvider(previewView.surfaceProvider) - ```
2. **Handle tap-to-focus**: -
```kotlin val factory = previewView.meteringPointFactory val point = factory.createPoint(x, y) // x, y from touch event val action = FocusMeteringAction.Builder(point, FocusMeteringAction.FLAG_AF).build() cameraControl?.startFocusAndMetering(action) - ```
@@ -80,7 +75,6 @@ Use `androidx.camera.compose.CameraXViewfinder`. 1. **Set up preview and SurfaceRequest**: -
```kotlin var surfaceRequest by remember { mutableStateOf(null) } @@ -89,14 +83,12 @@ Use `androidx.camera.compose.CameraXViewfinder`. setSurfaceProvider { request -> surfaceRequest = request } } } - ```
2. **Render viewfinder**: -
```kotlin surfaceRequest?.let { request -> @@ -106,14 +98,12 @@ Use `androidx.camera.compose.CameraXViewfinder`. modifier = Modifier ) } - ```
3. **Handle tap-to-focus in Compose**: -
```kotlin // Inside your tap gesture handler... @@ -125,14 +115,12 @@ Use `androidx.camera.compose.CameraXViewfinder`. val point = factory.createPoint(surfaceCoords.x, surfaceCoords.y) val action = FocusMeteringAction.Builder(point, FocusMeteringAction.FLAG_AF).build() cameraControl?.startFocusAndMetering(action) - ```
4. **Update target rotation for Compose**: -
```kotlin LaunchedEffect(configuration) { @@ -142,16 +130,15 @@ Use `androidx.camera.compose.CameraXViewfinder`. preview.targetRotation = rotation } } - ```
## Capture a photo -Use the `ImageCapture` use case to take the picture. The `ImageProxy` handles rotation directly. +Use the `ImageCapture` use case to take the picture. The `ImageProxy` handles +rotation directly. -
```kotlin imageCapture.takePicture( @@ -183,16 +170,15 @@ imageCapture.takePicture( } } ) - ```
## Switch cameras -To flip between front and rear cameras, change the `CameraSelector` and retrigger the `ProcessCameraProvider` logic. +To flip between front and rear cameras, change the `CameraSelector` and +retrigger the `ProcessCameraProvider` logic. -
```kotlin lensFacing = if (lensFacing == CameraSelector.LENS_FACING_BACK) { @@ -200,7 +186,6 @@ lensFacing = if (lensFacing == CameraSelector.LENS_FACING_BACK) { } else { CameraSelector.LENS_FACING_BACK } - ```
diff --git a/camera/camerax/references/camera2-to-camerax.md b/camera/camerax/references/camera2-to-camerax.md index eee1510..5b21f87 100644 --- a/camera/camerax/references/camera2-to-camerax.md +++ b/camera/camerax/references/camera2-to-camerax.md @@ -1,6 +1,10 @@ -Camera2 offers granular control but introduces boilerplate: managing `CameraDevice` states, `CameraCaptureSession` lifecycles, background threads, `HandlerThread`, and manual orientation calculations. +Camera2 offers granular control but introduces boilerplate: managing +`CameraDevice` states, `CameraCaptureSession` lifecycles, background threads, +`HandlerThread`, and manual orientation calculations. -CameraX simplifies this by binding high-level `UseCase`s such as `Preview`, `ImageCapture`, and `ImageAnalysis` directly to Android lifecycles, handling thread management and device-specific workarounds automatically. +CameraX simplifies this by binding high-level `UseCase`s such as `Preview`, +`ImageCapture`, and `ImageAnalysis` directly to Android lifecycles, handling +thread management and device-specific workarounds automatically. *** ** * ** *** @@ -17,9 +21,10 @@ Migrating to CameraX removes manual setup code: ## Initialize `ProcessCameraProvider` -Request the `ProcessCameraProvider` and bind your use cases to the `LifecycleOwner` activity or fragment. This replaces the `CameraManager.openCamera` flow. +Request the `ProcessCameraProvider` and bind your use cases to the +`LifecycleOwner` activity or fragment. This replaces the +`CameraManager.openCamera` flow. -
```kotlin val context = LocalContext.current @@ -51,7 +56,6 @@ LaunchedEffect(context, lifecycleOwner) { imageAnalysis ) } - ```
@@ -60,17 +64,17 @@ LaunchedEffect(context, lifecycleOwner) { ## Implement the preview and tap-to-focus -CameraX handles surface configuration automatically. Choose based on your UI toolkit: +CameraX handles surface configuration automatically. Choose based on your UI +toolkit: ### Option A: For Android Views -Use `androidx.camera.view.PreviewView` in your layout, and bind it to the `Preview` use case. +Use `androidx.camera.view.PreviewView` in your layout, and bind it to the +`Preview` use case. -
```kotlin preview.setSurfaceProvider(previewView.surfaceProvider) - ```
@@ -79,7 +83,6 @@ preview.setSurfaceProvider(previewView.surfaceProvider) Use `androidx.camera.compose.CameraXViewfinder`. -
```kotlin var surfaceRequest by remember { mutableStateOf(null) } @@ -88,7 +91,6 @@ val preview = remember { setSurfaceProvider { request -> surfaceRequest = request } } } - ```
@@ -97,9 +99,10 @@ val preview = remember { ## Capture a photo -Replace `ImageReader` capture flows and `CaptureRequest.Builder.TEMPLATE_STILL_CAPTURE` with the `ImageCapture` use case. CameraX handles the rotation natively using the returned `ImageProxy`. +Replace `ImageReader` capture flows and +`CaptureRequest.Builder.TEMPLATE_STILL_CAPTURE` with the `ImageCapture` use +case. CameraX handles the rotation natively using the returned `ImageProxy`. -
```kotlin imageCapture.takePicture( @@ -131,7 +134,6 @@ imageCapture.takePicture( } } ) - ```
@@ -140,9 +142,9 @@ imageCapture.takePicture( ## Implement image analysis -If you were using `ImageReader` in Camera2 to access raw frames, e.g., for QR scanning or ML,, replace it with the CameraX `ImageAnalysis` use case. +If you were using `ImageReader` in Camera2 to access raw frames, e.g., for QR +scanning or ML,, replace it with the CameraX `ImageAnalysis` use case. -
```kotlin imageAnalysis.setAnalyzer(cameraExecutor) { imageProxy -> @@ -155,7 +157,6 @@ imageAnalysis.setAnalyzer(cameraExecutor) { imageProxy -> imageProxy.close() } } - ```
@@ -164,9 +165,10 @@ imageAnalysis.setAnalyzer(cameraExecutor) { imageProxy -> ## Use Camera2 interop -If your app requires specific Camera2 configuration options, such as custom exposure modes or flash settings, that aren't exposed directly in CameraX, use `Camera2Interop` to apply them to your CameraX use cases. +If your app requires specific Camera2 configuration options, such as custom +exposure modes or flash settings, that aren't exposed directly in CameraX, +use `Camera2Interop` to apply them to your CameraX use cases. -
```kotlin // Use Camera2Interop to set Camera2-specific capture options @@ -178,7 +180,6 @@ extender.setCaptureRequestOption( CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH ) - ```
diff --git a/camera/camerax/references/expert-blueprints.md b/camera/camerax/references/expert-blueprints.md index 753347b..e1f2f25 100644 --- a/camera/camerax/references/expert-blueprints.md +++ b/camera/camerax/references/expert-blueprints.md @@ -1,4 +1,6 @@ -Complex camera features often fail due to "Agent Stall" or timeouts when attempted in a single turn. Use these blueprints to break tasks into manageable phases. +Complex camera features often fail due to "Agent Stall" or timeouts when +attempted in a single turn. Use these blueprints to break tasks into manageable +phases. ## Manual controls @@ -52,4 +54,5 @@ Complex camera features often fail due to "Agent Stall" or timeouts when attempt ## Low-light capture -For guidance on Night Mode Extensions and Low Light Boost, [low-light.md](https://developer.android.com/agents/skills/camera/camerax/references/low-light). \ No newline at end of file +For guidance on Night Mode Extensions and Low Light Boost, +[low-light.md](https://developer.android.com/agents/skills/camera/camerax/references/low-light). \ No newline at end of file diff --git a/camera/camerax/references/foldables.md b/camera/camerax/references/foldables.md index f0dd882..22d378a 100644 --- a/camera/camerax/references/foldables.md +++ b/camera/camerax/references/foldables.md @@ -1,4 +1,6 @@ -Foldable devices introduce unique challenges for camera applications, including dynamic layout changes, multiple display orientations, and physical device postures, such as tabletop and book modes. +Foldable devices introduce unique challenges for camera applications, including +dynamic layout changes, multiple display orientations, and physical device +postures, such as tabletop and book modes. ## Manage fold states and postures @@ -14,9 +16,9 @@ Foldable devices introduce unique challenges for camera applications, including ### Detect posture changes -Use the Jetpack WindowManager library to observe the device's hinge state and fold layout. +Use the Jetpack WindowManager library to observe the device's hinge state and +fold layout. -
```kotlin lifecycleScope.launch { @@ -31,14 +33,15 @@ lifecycleScope.launch { } } } - ```
### Handle tabletop mode -In Tabletop mode, horizontal fold, you should move the viewfinder to the top half of the screen and the controls to the bottom half to prevent the user from seeing a "bent" image. +In Tabletop mode, horizontal fold, you should move the viewfinder to the top +half of the screen and the controls to the bottom half to prevent the user from +seeing a "bent" image. - **Identify orientation:** Check `FoldingFeature.orientation`. - **Calculate geometry:** Use `FoldingFeature.bounds` to identify the hinge's physical location on the screen. @@ -46,9 +49,9 @@ In Tabletop mode, horizontal fold, you should move the viewfinder to the top hal ### Coordinate mapping and `Viewport` -When the UI layout changes due to a fold, you **must** update the `Viewport` to ensure that tap-to-focus and image capture coordinates remain accurate. +When the UI layout changes due to a fold, you **must** update the `Viewport` to +ensure that tap-to-focus and image capture coordinates remain accurate. -
```kotlin val viewport = ViewPort.Builder(Rational(viewfinder.width, viewfinder.height), display.rotation) @@ -59,14 +62,14 @@ val useCaseGroup = UseCaseGroup.Builder() .addUseCase(preview) .setViewPort(viewport) .build() - ```
### Rear display mode -Some foldables allow using the rear camera with the cover display while the device is unfolded. +Some foldables allow using the rear camera with the cover display while the +device is unfolded. - **Verification:** If available through OEM SDKs or Android 14 (API level 34) or higher, check `DeviceState.REAR_DISPLAY_STATE`. - **Logic:** Handle preview detachment and reattachment on different display surfaces with varying aspect ratios. diff --git a/camera/camerax/references/immutability.md b/camera/camerax/references/immutability.md index 30e11f6..1dade21 100644 --- a/camera/camerax/references/immutability.md +++ b/camera/camerax/references/immutability.md @@ -1,8 +1,11 @@ -Many Android APIs are designed with immutability in mind to prevent race conditions in async environments. However, this often trips up developers used to mutable builder patterns. +Many Android APIs are designed with immutability in mind to prevent race +conditions in async environments. However, this often trips up developers used +to mutable builder patterns. ## Common immutable classes -The following classes use fluent APIs that **return a new instance**. You must reassign the variable. +The following classes use fluent APIs that **return a new instance**. You must +reassign the variable. | Class | Methods that return a new instance | Result if not reassigned | |---|---|---| @@ -17,7 +20,6 @@ The following classes use fluent APIs that **return a new instance**. You must r To set up video recording, use the following code: -
```kotlin // WRONG @@ -40,7 +42,6 @@ run { pending = pending.withAudioEnabled() // Reassignment val active = pending.start(exec, listener) } - ```
@@ -49,13 +50,11 @@ run { To set up the viewport, use the following code: -
```kotlin val viewport = ViewPort.Builder(Rational(width, height), displayRotation) .setScaleType(ViewPort.FILL_CENTER) .build() - ```
\ No newline at end of file diff --git a/camera/camerax/references/low-light.md b/camera/camerax/references/low-light.md index 3650fa5..e03e00c 100644 --- a/camera/camerax/references/low-light.md +++ b/camera/camerax/references/low-light.md @@ -1,4 +1,5 @@ -This guide covers implementing low-light features using **Night mode extensions** and **Low Light Boost (LLB)**. +This guide covers implementing low-light features using **Night mode +extensions** and **Low Light Boost (LLB)**. ## Choosing the right tool @@ -12,13 +13,13 @@ This guide covers implementing low-light features using **Night mode extensions* ## Night mode extension -CameraX Extensions provide access to the device's built-in computational photography pipeline. +CameraX Extensions provide access to the device's built-in computational +photography pipeline. ### Basic setup To set up the extension, initialize the extension manager: -
```kotlin // Use ListenableFuture.await() extension function for coroutine support @@ -29,7 +30,6 @@ if (extensionsManager.isExtensionAvailable(cameraSelector, ExtensionMode.NIGHT)) ) cameraProvider.bindToLifecycle(lifecycleOwner, nightSelector, imageCapture, preview) } - ```
@@ -42,7 +42,6 @@ if (extensionsManager.isExtensionAvailable(cameraSelector, ExtensionMode.NIGHT)) val imageCapture = ImageCapture.Builder() .setPostviewEnabled(true) .build() - ``` - **Extension strength** : Let users control the intensity of the night effect. @@ -51,7 +50,6 @@ if (extensionsManager.isExtensionAvailable(cameraSelector, ExtensionMode.NIGHT)) val extensionsManager = ExtensionsManager.getInstanceAsync(context, cameraProvider).await() val extensionsControl = extensionsManager.getCameraExtensionsControl(camera.cameraControl) extensionsControl?.setExtensionStrength(strength) - ``` - **Capture progress** : Show a UI progress bar for long exposures. @@ -63,7 +61,6 @@ if (extensionsManager.isExtensionAvailable(cameraSelector, ExtensionMode.NIGHT)) } catch (e: ImageCaptureException) { // Handle capture failure } - ``` *** ** * ** *** @@ -74,7 +71,8 @@ LLB is designed for preview and video streams where you prefer high frame rates. ### AE mode -The built-in CameraX way to prioritize brightness. It modifies the hardware's auto-exposure algorithm. +The built-in CameraX way to prioritize brightness. It modifies the hardware's +auto-exposure algorithm. - **Activation** : Use `CameraControl.enableLowLightBoostAsync`. - **Implementation** : @@ -82,7 +80,6 @@ The built-in CameraX way to prioritize brightness. It modifies the hardware's au ```kotlin // Enable Low Light Boost (LLB) natively in CameraX 1.4+ camera.cameraControl.enableLowLightBoostAsync(true) - ``` - **Monitoring** : Observe `CameraInfo.lowLightBoostState` to track when the hardware actively applies the enhancement. @@ -115,7 +112,6 @@ To implement Google Play services LLB, follow these core steps: .addUseCase(videoCapture) .addEffect(effect) .build() - ``` 4. **Scene detection** : Use `session.setSceneDetectorCallback` to receive `boostStrength` updates for real-time UI indicators. diff --git a/camera/camerax/references/mlkit-spatial.md b/camera/camerax/references/mlkit-spatial.md index 0b29bb2..833daf1 100644 --- a/camera/camerax/references/mlkit-spatial.md +++ b/camera/camerax/references/mlkit-spatial.md @@ -1,4 +1,6 @@ -When you use ML Kit for features such as face mesh, object detection, or pose detection, the most common failure point is the coordinate disparity between the analysis image and the viewfinder UI. +When you use ML Kit for features such as face mesh, object detection, or pose +detection, the most common failure point is the coordinate disparity between the +analysis image and the viewfinder UI. ## The mapping mindset @@ -14,23 +16,25 @@ When you use ML Kit for features such as face mesh, object detection, or pose de ### Coordinate transformation matrix -Android provides the `Viewport` and `UseCaseGroup` APIs to calculate the transformation matrix automatically. **Don't** calculate aspect ratio scaling manually. +Android provides the `Viewport` and `UseCaseGroup` APIs to calculate the +transformation matrix automatically. **Don't** calculate aspect ratio scaling +manually. -
```kotlin val transform = previewView.viewPort?.let { viewPort -> // Use CameraX's built-in coordinate mapper viewPort.getTransformationMatrix(imageProxy.imageInfo.rotationDegrees) } - ```
### Handling the "double rotation" bug -ML Kit results, bounding boxes, are relative to the **rotated buffer**. If the device is in portrait, the buffer is often 480x640, landscape, but the screen is 1080x1920. +ML Kit results, bounding boxes, are relative to the **rotated buffer**. If the +device is in portrait, the buffer is often 480x640, landscape, but the screen +is 1080x1920. To map the coordinates, use the following workflow: @@ -40,20 +44,20 @@ To map the coordinates, use the following workflow: ### Face mesh and pose normalization -For high-precision spatial analysis, for example, "Is the user's hand at a specific screen button?", use **normalized coordinates from 0.0 to 1.0**. +For high-precision spatial analysis, for example, "Is the user's hand at a +specific screen button?", use **normalized coordinates from 0.0 to 1.0**. -
```kotlin // Example: Converting a Pose landmark to a Screen Coordinate val screenX = landmark.position.x / analysisWidth * screenWidth val screenY = landmark.position.y / analysisHeight * screenHeight - ```
-**Warning** : Always account for **mirrored lenses** . If the `LENS_FACING_FRONT` is used, you must flip the X-coordinate: `actualX = screenWidth - screenX`. +**Warning** : Always account for **mirrored lenses** . If the `LENS_FACING_FRONT` +is used, you must flip the X-coordinate: `actualX = screenWidth - screenX`. ### Overlays and canvas clipping diff --git a/camera/camerax/references/modern-apis.md b/camera/camerax/references/modern-apis.md index 32f0d56..616d549 100644 --- a/camera/camerax/references/modern-apis.md +++ b/camera/camerax/references/modern-apis.md @@ -1,4 +1,5 @@ -Always prefer these various abstractions over legacy Camera2 or early CameraX implementations. +Always prefer these various abstractions over legacy Camera2 or early CameraX +implementations. ## Compare APIs @@ -37,4 +38,5 @@ Add the following dependencies to your `libs.versions.toml` file: androidx-camera-extensions = { group = "androidx.camera", name = "camera-extensions", version.ref = "camerax" } -Refer to the official [CameraX Release Notes](https://developer.android.com/jetpack/androidx/releases/camera) for the stable versions. \ No newline at end of file +Refer to the official [CameraX Release Notes](https://developer.android.com/jetpack/androidx/releases/camera) for the +stable versions. \ No newline at end of file diff --git a/camera/camerax/references/testing.md b/camera/camerax/references/testing.md index 2c24bc3..972f285 100644 --- a/camera/camerax/references/testing.md +++ b/camera/camerax/references/testing.md @@ -1,4 +1,6 @@ -Automated testing for camera features is notoriously difficult because you can't easily mock physical hardware, lighting, or motion. This guide provides patterns for reliable, hermetic camera tests. +Automated testing for camera features is notoriously difficult because you +can't easily mock physical hardware, lighting, or motion. This guide provides +patterns for reliable, hermetic camera tests. ## Develop a testing mindset @@ -15,41 +17,42 @@ Automated testing for camera features is notoriously difficult because you can't ### Fakes over mocks -**Don't use Mockito.** Relying on mocks for complex, rapidly changing interfaces like `ImageProxy` or `CameraInfo` makes tests brittle. Instead, build "Fake" implementations that verify state rather than behavior. +**Don't use Mockito.** Relying on mocks for complex, rapidly changing interfaces +like `ImageProxy` or `CameraInfo` makes tests brittle. Instead, build "Fake" +implementations that verify state rather than behavior. -
```kotlin // Create a Fake ImageProxy for ML Testing (Fakes over Mocks) val fakeImage = FakeImageProxy(w = 640, h = 480) // Feed the fake buffer into your analyzer - ```
### Mock camera capabilities -Use `FakeAppConfig` from `androidx.camera:camera-testing` to simulate specific hardware constraints in tests, such as a device without a flash. +Use `FakeAppConfig` from `androidx.camera:camera-testing` to simulate specific +hardware constraints in tests, such as a device without a flash. -
```kotlin // Use awaitInstance() extension function for coroutine-based provider retrieval val cameraProvider = ProcessCameraProvider.awaitInstance(context) - ```
### Use Truth assertions -Use Google Truth, `assertThat`, instead of standard JUnit assertions. It provides more readable assertion chains and useful failure messages. +Use Google Truth, `assertThat`, instead of standard JUnit assertions. It +provides more readable assertion chains and useful failure messages. ### Test asynchronous lifecycles -Camera initialization is asynchronous. Use `IdlingResource` to ensure your test waits for the `UseCase` to be bound before asserting. +Camera initialization is asynchronous. Use `IdlingResource` to ensure +your test waits for the `UseCase` to be bound before asserting. To test asynchronous lifecycles, use the following pattern: diff --git a/camera/camerax/references/thermals.md b/camera/camerax/references/thermals.md index 0447f27..41cfe19 100644 --- a/camera/camerax/references/thermals.md +++ b/camera/camerax/references/thermals.md @@ -1,4 +1,6 @@ -Camera operations are among the most power-intensive tasks on mobile devices. Without proactive management, the system throttle hardware, drop frames, or force-close the camera app. +Camera operations are among the most power-intensive tasks on mobile devices. +Without proactive management, the system throttle hardware, drop frames, or +force-close the camera app. ## The thermal management strategy @@ -14,9 +16,9 @@ Camera operations are among the most power-intensive tasks on mobile devices. Wi ### Stream use case optimization -Android 13 (API level 33) introduced `StreamUseCase`. This is the **single most effective** way to tell the hardware how to balance quality versus power. +Android 13 (API level 33) introduced `StreamUseCase`. This is the **single most +effective** way to tell the hardware how to balance quality versus power. -
```kotlin // In CameraX: Set the hint on your Use Case @@ -28,7 +30,6 @@ val preview = Preview.Builder() ) } .build() - ```
@@ -42,9 +43,9 @@ Review the following key use cases for stream optimization: ### Monitor thermal status -Don't wait for a crash. Monitor the `PowerManager` status and react before `THERMAL_STATUS_CRITICAL`. +Don't wait for a crash. Monitor the `PowerManager` status and react before +`THERMAL_STATUS_CRITICAL`. -
```kotlin val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager @@ -62,7 +63,6 @@ powerManager.addThermalStatusListener { status -> } } } - ```
diff --git a/camera/camerax/references/wear-os.md b/camera/camerax/references/wear-os.md index 11ae45f..4a942db 100644 --- a/camera/camerax/references/wear-os.md +++ b/camera/camerax/references/wear-os.md @@ -1,4 +1,6 @@ -Developing camera features for Wear OS is rarely about the watch's own lens, if it even has one. It's almost always about creating a **Remote Viewfinder** to control the phone's camera. +Developing camera features for Wear OS is rarely about the watch's own lens, if +it even has one. It's almost always about creating a **Remote Viewfinder** to +control the phone's camera. ## The Wear OS remote mindset @@ -15,7 +17,8 @@ Developing camera features for Wear OS is rarely about the watch's own lens, if ### The circular UI challenge -Wear OS devices are often round. Standard rectangular layouts clip corner buttons. +Wear OS devices are often round. Standard rectangular layouts clip corner +buttons. Follow these blueprint recommendations: @@ -27,7 +30,6 @@ Follow these blueprint recommendations: You can't send a raw 60 fps stream over Bluetooth. compress and throttle. -
```kotlin // Example: Sending a viewfinder frame to the watch @@ -39,31 +41,30 @@ if (bitmap != null) { } Wearable.getDataClient(context).putDataItem(request.asPutDataRequest()) } - - ```
-**Optimization** : Cap the watch preview at **10-15 fps** to preserve battery and bandwidth. +**Optimization** : Cap the watch preview at **10-15 fps** to preserve battery and +bandwidth. ### Remote triggers and syncing -Use the `MessageClient` for low-latency commands like "Take Photo" or "Switch Camera." +Use the `MessageClient` for low-latency commands like "Take Photo" or "Switch +Camera." -
```kotlin // Watch sends a trigger to the phone Wearable.getMessageClient(context).sendMessage(nodeId, "/camera/capture", null) - ```
### Rotary input support -On devices that support it, use the physical crown, Rotary Input, to control **Zoom** or **Exposure**. +On devices that support it, use the physical crown, Rotary Input, to control +**Zoom** or **Exposure**. *** ** * ** *** diff --git a/camera/camerax/references/xr.md b/camera/camerax/references/xr.md index 1edfddf..59fae32 100644 --- a/camera/camerax/references/xr.md +++ b/camera/camerax/references/xr.md @@ -1,4 +1,5 @@ -Developing camera features for XR devices, headsets, and AR glasses requires a shift from 2D pixel-pushing to 3D spatial awareness. +Developing camera features for XR devices, headsets, and AR glasses requires a +shift from 2D pixel-pushing to 3D spatial awareness. ## Understand the XR development mindset @@ -14,7 +15,8 @@ Developing camera features for XR devices, headsets, and AR glasses requires a s ### API selection -On XR devices, standard `CameraX` implementations are often restricted or insufficient. Always use spatial software development kits (SDKs): +On XR devices, standard `CameraX` implementations are often restricted or +insufficient. Always use spatial software development kits (SDKs): - **ARCore**: Use ARCore for plane detection, depth sensing, and motion tracking. - **OpenXR**: Use OpenXR as the cross-platform standard for VR and AR rendering and input. @@ -24,13 +26,13 @@ On XR devices, standard `CameraX` implementations are often restricted or insuff Unlike a 2D viewport, XR passthrough is often system-managed. -**\[Key requirement\] Frame synchronization**: Synchronize your application's frame clock with the headset's head-mounted display (HMD) pose. +**\[Key requirement\] Frame synchronization**: Synchronize your application's +frame clock with the headset's head-mounted display (HMD) pose. ```kotlin // Example: Querying the spatial pose for the current camera frame val headPose = xrSession.getHeadPose(frameTime) val projectionMatrix = headPose.getProjectionMatrix(eyeIndex) - ```
diff --git a/device-ai/appfunctions/SKILL.md b/device-ai/appfunctions/SKILL.md index 5ad96e6..17f3f91 100644 --- a/device-ai/appfunctions/SKILL.md +++ b/device-ai/appfunctions/SKILL.md @@ -9,7 +9,7 @@ description: Analyzes Android apps to identify key user workflows for AppFunctio license: Complete terms in LICENSE.txt metadata: author: Google LLC - last-updated: '2026-08-06' + last-updated: '2026-08-14' keywords: - AppFunctions - Kotlin @@ -20,19 +20,29 @@ metadata: - MCP --- -Analyzes Android apps to identify key user workflows for AppFunctions such as creating a note, playing media, or sending an automated or AI agent triggered message, voice commands, or system shortcuts, without needing to open the app UI. +Analyzes Android apps to identify key user workflows for AppFunctions such as +creating a note, playing media, or sending an automated or AI agent triggered +message, voice commands, or system shortcuts, without needing to open the app +UI. -Generates Kotlin code to expose these workflows to the Android system, allowing agents to discover and execute them on-device. +Generates Kotlin code to expose these workflows to the Android system, +allowing agents to discover and execute them on-device. -Also refines KDoc documentation to ensure AI agents correctly understand and use the provided functionality. +Also refines KDoc documentation to ensure AI agents correctly +understand and use the provided functionality. ## Prerequisites -The app must **`targetSdk 36`** or newer and use **`compileSdk 37`** or newer as AppFunctions, part of the Android platform API, are available from Android 16 onwards. Always use the Jetpack library because it handles backward compatibility. +The app must **`targetSdk 36`** or newer and use **`compileSdk 37`** or newer as +AppFunctions, part of the Android platform API, are available from Android 16 +onwards. +Always use the Jetpack library because it handles backward compatibility. ## Workflows -This skill enables the caller to discover features that will be provided to system agents, implement these with AppFunctions, improve function description for agents, and use ADB commands for local evaluation and testing. +This skill enables the caller to discover features that will be provided to +system agents, implement these with AppFunctions, improve function description +for agents, and use ADB commands for local evaluation and testing. The full AppFunction development flow consists of these four steps: diff --git a/device-ai/appfunctions/references/adb-interaction-testing.md b/device-ai/appfunctions/references/adb-interaction-testing.md index b7fac4a..b487923 100644 --- a/device-ai/appfunctions/references/adb-interaction-testing.md +++ b/device-ai/appfunctions/references/adb-interaction-testing.md @@ -1,13 +1,16 @@ -Provides commands to interact with AppFunctions on a connected device or emulator using ADB for AppFunction testing and debugging. +Provides commands to interact with AppFunctions on a connected device or +emulator using ADB for AppFunction testing and debugging. ## Instructions ### Scenario 1: List app functions -Use this scenario when you want to see which app functions are registered on the device. +Use this scenario when you want to see which app functions are registered on +the device. 1. **List all functions** : To view all registered app functions in JSON format, run `adb shell cmd app_function list-app-functions`. -2. **Filter by package** : To view functions for a specific package, pipe the output to `grep` or a JSON tool: `adb shell cmd app_function list-app-functions | grep `. +2. **Filter by package** : To view functions for a specific package, pipe the output to `grep` or a JSON tool: `adb shell cmd app_function + list-app-functions | grep `. ### Scenario 2: Invoke app functions @@ -16,28 +19,38 @@ If you want to test the execution of an app function, use this scenario. 1. **Analyze description** : Before invoking, you must read the `description` field for the function in the `list-app-functions` output. This often contains critical usage constraints, required workflows, or disambiguation rules. 2. **Follow constraints**: Follow all instructions in the description, such as asking the user to disambiguate or calling another tool first. 3. **Format parameters** : Format the `--parameters` argument as a valid JSON string that represents the function's input arguments. -4. **Execute function** : Use `adb shell cmd app_function execute-app-function --package --function --parameters ''`. +4. **Execute function** : Use `adb shell cmd app_function execute-app-function + --package --function + --parameters ''`. 5. **Handle response** : The command returns the result as a JSON string. To get brief YAML output, use `--brief-yaml`. ### Scenario 3: Manage function state If you need to enable or disable an app function for testing, use this scenario. -1. **Set enabled state** : Use `adb shell cmd app_function set-enabled --package --function --state `. +1. **Set enabled state** : Use `adb shell cmd app_function set-enabled + --package + --function + --state `. ## Critical constraints ### Follow metadata descriptions -**Mandatory** : The `description` field in the app function metadata is a set of instructions for the LLM. If a description says to "disambiguate with the user" or "call another function first," you must perform those steps before execution. +**Mandatory** : The `description` field in the app function metadata is a set of +instructions for the LLM. If a description says to "disambiguate with the user" +or "call another function first," you must perform those steps before execution. ### JSON escaping -**Critical** : When passing JSON using `adb shell`, always wrap the JSON string in single quotes to prevent the shell from interpreting special characters or spaces. Example: `--parameters '{"key": "value"}'`. +**Critical** : When passing JSON using `adb shell`, always wrap the JSON string +in single quotes to prevent the shell from interpreting special characters or +spaces. Example: `--parameters '{"key": "value"}'`. ### Device availability -The `app_function` service must be available on the device. If `cmd: Can't find service: app_function` is returned, the device doesn't support this feature. +The `app_function` service must be available on the device. If `cmd: Can't find +service: app_function` is returned, the device doesn't support this feature. ## Examples @@ -45,7 +58,8 @@ The `app_function` service must be available on the device. If `cmd: Can't find adb shell cmd app_function help -If executing the preceding command returns a help page, use the commands and parameters provided to guide the ADB interaction testing tool interactions. +If executing the preceding command returns a help page, use the commands and +parameters provided to guide the ADB interaction testing tool interactions. ### Example 2: List all registered app functions @@ -77,4 +91,5 @@ If executing the preceding command returns a help page, use the commands and par **Cause**: The function ID or package name is incorrect. -**Solution** : Run `list-app-functions` and search for the relevant identifiers in the JSON output. \ No newline at end of file +**Solution** : Run `list-app-functions` and search for the relevant identifiers +in the JSON output. \ No newline at end of file diff --git a/device-ai/appfunctions/references/context.md b/device-ai/appfunctions/references/context.md index 9d18e2c..dc49d25 100644 --- a/device-ai/appfunctions/references/context.md +++ b/device-ai/appfunctions/references/context.md @@ -2,10 +2,25 @@ Defines the ubiquitous language for the Android AppFunctions skill suite. ## Architecture and versioning -`AppFunctionServiceEntryPoint` API: This compile-time AppFunctions architecture was introduced in version 1.0.0-alpha10. You annotate a wrapper service extending `AppFunctionService` with `@AppFunctionServiceEntryPoint`. KSP then generates XML metadata and routes services at compile time. +`AppFunctionServiceEntryPoint` API: +This compile-time AppFunctions architecture was introduced in version +1.0.0-alpha10. You annotate a wrapper service extending +`AppFunctionService` with `@AppFunctionServiceEntryPoint`. KSP then generates +XML metadata and routes services at compile time. -Legacy manual provider API: This deprecated AppFunctions architecture applies to version 1.0.0-alpha09 and earlier. Applications implement `AppFunctionConfiguration.Provider` on the `Application` class. Methods require `AppFunctionContext` as the first parameter. Projects depend on a standalone `appfunctions-service` library. +Legacy manual provider API: +This deprecated AppFunctions architecture applies to version +1.0.0-alpha09 and earlier. Applications implement +`AppFunctionConfiguration.Provider` on the `Application` class. Methods +require `AppFunctionContext` as the first parameter. Projects depend on +a standalone `appfunctions-service` library. ## Patterns -Service entry point pattern: Version 1.0.0-alpha10 introduced this architectural pattern. You declare `@AppFunction` methods directly inside an abstract class extending `AppFunctionService`. This class uses `@AppFunctionServiceEntryPoint` and `@AndroidEntryPoint` annotations. These annotations let you inject data sources or repositories directly, without an intermediate business logic delegation layer. \ No newline at end of file +Service entry point pattern: +Version 1.0.0-alpha10 introduced this architectural pattern. You +declare `@AppFunction` methods directly inside an abstract class extending +`AppFunctionService`. This class uses `@AppFunctionServiceEntryPoint` and +`@AndroidEntryPoint` annotations. These annotations let you inject data +sources or repositories directly, without an intermediate business logic +delegation layer. \ No newline at end of file diff --git a/device-ai/appfunctions/references/feature-discovery-analysis.md b/device-ai/appfunctions/references/feature-discovery-analysis.md index 4adb255..583f67c 100644 --- a/device-ai/appfunctions/references/feature-discovery-analysis.md +++ b/device-ai/appfunctions/references/feature-discovery-analysis.md @@ -13,11 +13,15 @@ Analyzes Android codebases to identify and recommend high-value AppFunctions. ### Tool-first thinking -Avoid recommending functions that are purely informational or redundant with existing system actions. Focus on "mutations" (writing data) or "rich queries" (finding specific entities). +Avoid recommending functions that are purely informational or redundant with +existing system actions. Focus on "mutations" (writing data) or "rich queries" +(finding specific entities). ### Security and privacy -Don't recommend exposing functions that handle raw credentials, financial secrets, or irreversible destructive actions without explicit user confirmation steps. +Don't recommend exposing functions that handle raw credentials, financial +secrets, or irreversible destructive actions without explicit user confirmation +steps. ## Examples @@ -25,7 +29,9 @@ Don't recommend exposing functions that handle raw credentials, financial secret **Recommended AppFunction** : `playArtistRadio` -**Rationale**: Lets you start a personalized music stream using a voice command, bypassing several layers of navigation in the "Search" and "Artist" menus. +**Rationale**: Lets you start a personalized music stream using a voice +command, bypassing several layers of navigation in the "Search" and "Artist" +menus. **Input required** : `artistName` as a `String`. @@ -33,22 +39,33 @@ Don't recommend exposing functions that handle raw credentials, financial secret **Recommended AppFunction** : `searchContacts` -**Rationale** : Serves as a "rich query" to resolve a human-readable contact name, email, or chat group to a unique identifier (`endpointValue`), which is a prerequisite before executing actions like sending messages or initiating calls. Also allows retrieving recently contacted entities when given a blank query. +**Rationale** : Serves as a "rich query" to resolve a human-readable contact +name, email, or chat group to a unique identifier (`endpointValue`), which is a +prerequisite before executing actions like sending messages or +initiating calls. Also allows retrieving recently contacted entities when given +a blank query. -**Input required** : Query as a `String`, and Filter Type as a `String` constrained to `"INDIVIDUAL"` or `"GROUP"`. +**Input required** : Query as a `String`, and Filter Type as a `String` +constrained to `"INDIVIDUAL"` or `"GROUP"`. ### Example 3: Chat app message sending **Recommended AppFunction** : `send` -**Rationale**: This mutation function lets you send text messages and optional image attachments to a contact or group using natural language commands, for example, "Tell Alice I'm running 5 minutes late." This eliminates multi-step UI navigation across contact lists and conversation threads. +**Rationale**: This mutation function lets you send text messages and +optional image attachments to a contact or group using natural language +commands, for example, "Tell Alice I'm running 5 minutes late." This +eliminates multi-step UI navigation across contact lists and conversation +threads. -**Input required** : Endpoint Value as a `String`, Message Body as a `String`, and Image URIs as an optional `List` of URIs. +**Input required** : Endpoint Value as a `String`, Message Body as a `String`, +and Image URIs as an optional `List` of URIs. ### Example 4: Chat app voice calling **Recommended AppFunction** : `makeCall` -**Rationale**: Lets you initiate voice calls hands-free to a contact or group using an AI agent without navigating the app's UI. +**Rationale**: Lets you initiate voice calls hands-free to a contact or +group using an AI agent without navigating the app's UI. **Input required** : `endpointValue` as a `String`. \ No newline at end of file diff --git a/device-ai/appfunctions/references/implementation-configuration.md b/device-ai/appfunctions/references/implementation-configuration.md index 4f8260e..fb9292c 100644 --- a/device-ai/appfunctions/references/implementation-configuration.md +++ b/device-ai/appfunctions/references/implementation-configuration.md @@ -1,19 +1,19 @@ -Specialized instructions for generating Kotlin implementations of AppFunctions, handling system-wide configuration, and managing build dependencies. +Specialized instructions for generating Kotlin implementations of AppFunctions, +handling system-wide configuration, and managing build dependencies. ## Instructions ### Step 1: Configure Gradle dependencies and KSP -Add the following to `build.gradle.kts`. App Functions requires the KSP (Kotlin Symbol Processing) plugin. +Add the following to `build.gradle.kts`. App Functions requires the KSP (Kotlin +Symbol Processing) plugin. 1. **Version check** : Use library version `1.0.0-alpha10` or later from maven.google.com. -
```kotlin implementation(libs.androidx.appfunctions) ksp(libs.androidx.appfunctions.compiler) - ```
@@ -24,9 +24,9 @@ ksp(libs.androidx.appfunctions.compiler) ### Step 2: Set up app metadata XML -Describe the app's capabilities to the LLM by defining `res/xml/app_metadata.xml`. +Describe the app's capabilities to the LLM by defining +`res/xml/app_metadata.xml`. -
```xml - ```
-Register the service and reference the app metadata in `AndroidManifest.xml` within the `` tag: +Register the service and reference the app metadata in `AndroidManifest.xml` +within the `` tag: -
```xml - ```
@@ -98,13 +96,16 @@ When generating Kotlin code for AppFunctions, you MUST adhere to these rules: ### Step 4: Set up dependency injection and service entry points -In version 1.0.0-alpha10 and later, App Functions use the compile-time `@AppFunctionServiceEntryPoint` architecture. Create an abstract class extending `AppFunctionService` annotated with `@AppFunctionServiceEntryPoint`. KSP generates the concrete service class and XML schema. +In version 1.0.0-alpha10 and later, App Functions use the compile-time +`@AppFunctionServiceEntryPoint` architecture. Create an abstract class extending +`AppFunctionService` annotated with `@AppFunctionServiceEntryPoint`. KSP +generates the concrete service class and XML schema. #### Recommended approach with Hilt -Annotate your service with `@AndroidEntryPoint` and inject your data repositories or use cases using standard `@Inject internal lateinit var`: +Annotate your service with `@AndroidEntryPoint` and inject your data +repositories or use cases using standard `@Inject internal lateinit var`: -
```kotlin @RequiresApi(36) @@ -125,16 +126,19 @@ abstract class BaseAppFunctionServiceHeader : AppFunctionService() { return messageRepository.send(name, endpointValue, messageBody) } } - ```
#### Framework-agnostic approach with alternative dependency injection or service locators -While Hilt is recommended, many Android applications implement AppFunctions with alternative dependency injection frameworks (like Koin, Anvil, or manual Service Locators). Because `AppFunctionService` inherits from Android `android.app.Service` (and therefore `Context`), you are able access your application's DI container directly through `applicationContext` in property getters or during service lifecycle execution: +While Hilt is recommended, many Android applications implement AppFunctions with +alternative dependency injection frameworks (like Koin, Anvil, or manual Service +Locators). Because `AppFunctionService` inherits from Android +`android.app.Service` (and therefore `Context`), you are able access your +application's DI container directly through `applicationContext` in property +getters or during service lifecycle execution: -
```kotlin @RequiresApi(36) @@ -159,14 +163,16 @@ abstract class BaseAppFunctionServiceLocator : AppFunctionService() { return messageRepository.send(name, endpointValue, messageBody) } } - ```
### Step 5: Architectural cleanliness -Don't attempt to make an `AppFunction` class or method OS-agnostic---App Functions are inherently part of the Android platform integration in `androidx.appfunctions`. For architectural cleanliness, use existing application functionality (such as existing repositories, use cases, or domain orchestrators) to execute the behavior within your `@AppFunction` methods rather than creating redundant abstraction layers around the OS service. +Don't attempt to make an `AppFunction` class or method OS-agnostic---App Functions are inherently part of the Android platform integration in `androidx.appfunctions`. For architectural cleanliness, use existing +application functionality (such as existing repositories, use cases, or domain +orchestrators) to execute the behavior within your `@AppFunction` methods rather +than creating redundant abstraction layers around the OS service.
@@ -174,17 +180,19 @@ Don't attempt to make an `AppFunction` class or method OS-agnostic---App Functio ### KSP compliance for serializables -**Critical constraints** : For `@AppFunctionSerializable` data classes, KSP only extracts documentation if it's written as inline KDoc directly for each property definition. Don't use class-level `@param` or `@property` tags. +**Critical constraints** : For `@AppFunctionSerializable` data classes, KSP +only extracts documentation if it's written as inline KDoc directly for each +property definition. Don't use class-level `@param` or `@property` tags. ### Package integrity -Configuration APIs and the `@AppFunction` annotation are located in `androidx.appfunctions`. +Configuration APIs and the `@AppFunction` annotation are located in +`androidx.appfunctions`. ## Examples ### Example: Serializable with inline KDoc -
```kotlin /** The parameter to create the task. */ @@ -206,7 +214,6 @@ data class Task( /** The content of the task. */ val content: String, ) - ```
@@ -215,7 +222,6 @@ data class Task( ### Example: Implementation detail -
```kotlin @RequiresApi(36) @@ -256,7 +262,6 @@ abstract class BaseTaskAppFunctionService : AppFunctionService() { // Maps internal TaskEntity private fun TaskEntity.toTask() = Task(id = id, title = title, content = description) } - ```
diff --git a/device-ai/appfunctions/references/kdoc-refinement-optimization.md b/device-ai/appfunctions/references/kdoc-refinement-optimization.md index 38ce18e..c0a3209 100644 --- a/device-ai/appfunctions/references/kdoc-refinement-optimization.md +++ b/device-ai/appfunctions/references/kdoc-refinement-optimization.md @@ -5,7 +5,8 @@ Optimizes AppFunction KDoc for AI agents and Model Context Protocol. ### Workflow: Agent-centric documentation 1. **Identify the core outcome** : Start the description with a strong imperative verb, for example, "Search", "Create", or "Update". Focus on the *user benefit*, not the code implementation. -2. **Workflow dependencies** : Explicitly state if another function must be called first using the standard phrase: **Required workflow: Call "Function A" first to "Objective"**. +2. **Workflow dependencies** : Explicitly state if another function must be called first using the standard phrase: **Required workflow: Call "Function + A" first to "Objective"**. 3. **Parameter documentation** : - For **functions** : Use specific `@param` tags. Isolate validation rules and default values here. - For **serializables** : Use inline KDoc directly for each property declaration. KSP **won't** extract documentation from class-level tags. @@ -13,7 +14,8 @@ Optimizes AppFunction KDoc for AI agents and Model Context Protocol. ### Workflow: Global app description for server instructions -When writing the `appfn:description` for `app_metadata.xml`, follow these instructions: +When writing the `appfn:description` for `app_metadata.xml`, follow these +instructions: 1. **Capture cross-function relationships**: Explain dependencies or sequences between tools, for example, "Always call 'authenticate' before fetching data.". 2. **Document operational patterns**: Guide the LLM on token-conserving usage, for example, "Use 'batch_update' over multiple 'update' calls." @@ -27,15 +29,19 @@ When writing the `appfn:description` for `app_metadata.xml`, follow these instru ### Descriptive, not imperative -Describe what the function *does* , not what the LLM *must* do. Avoid phrases like "You must call this..." in favor of "This function provides...". +Describe what the function *does* , not what the LLM *must* do. Avoid phrases +like "You must call this..." in favor of "This function provides...". ### No "fluff" -Remove conversational padding like "This method is used to..." or "Helpful for...". Be concise and technical. +Remove conversational padding like "This method is used to..." or "Helpful +for...". Be concise and technical. ### Inline KDoc for serializables -**Mandatory** : For `@AppFunctionSerializable` classes, documentation must be inline for each property. KSP ignores class-level `@param` or `@property` tags for these classes. +**Mandatory** : For `@AppFunctionSerializable` classes, documentation must be +inline for each property. KSP ignores class-level `@param` or `@property` tags +for these classes. ## Examples diff --git a/device-ai/appfunctions/references/migrate-to-service-entry-point.md b/device-ai/appfunctions/references/migrate-to-service-entry-point.md index a0528e9..f350f61 100644 --- a/device-ai/appfunctions/references/migrate-to-service-entry-point.md +++ b/device-ai/appfunctions/references/migrate-to-service-entry-point.md @@ -1,4 +1,7 @@ -Follow this systematic procedure to migrate Android applications that use the AppFunctions API in version 1.0.0-alpha09 and lower to the compile-time `@AppFunctionServiceEntryPoint` architecture introduced in version `1.0.0-alpha10`. +Follow this systematic procedure to migrate Android applications that use the +AppFunctions API in version 1.0.0-alpha09 and lower to the compile-time +`@AppFunctionServiceEntryPoint` architecture introduced in version +`1.0.0-alpha10`. *** ** * ** *** @@ -20,7 +23,9 @@ In version `1.0.0-alpha10` featuring `@AppFunctionServiceEntryPoint`: ### Strict migration requirements from 1.0.0-alpha09 to 1.0.0-alpha10 -When focusing solely on the mandatory API changes required by the new `@AppFunctionServiceEntryPoint` architecture, the migration consists of four strict requirements that you must complete: +When focusing solely on the mandatory API changes required by the new +`@AppFunctionServiceEntryPoint` architecture, the migration consists of four +strict requirements that you must complete: 1. **Build dependency consolidation** : Remove the merged `appfunctions-service` dependency while retaining core `appfunctions` and the KSP compiler. 2. **Service wrapper creation** : Replace the legacy `AppFunctionConfiguration.Provider` on the `Application` class with an abstract class extending `AppFunctionService`, annotated with `@AppFunctionServiceEntryPoint`. @@ -33,7 +38,10 @@ When focusing solely on the mandatory API changes required by the new `@AppFunct ### Consolidate AppFunctions build dependencies -Remove the standalone `appfunctions-service` library from your module build files like `build.gradle.kts` and version catalog like `libs.versions.toml`. In version `1.0.0-alpha10`, all core service capabilities are consolidated directly within the main `appfunctions` artifact. +Remove the standalone `appfunctions-service` library from your module build +files like `build.gradle.kts` and version catalog like `libs.versions.toml`. In +version `1.0.0-alpha10`, all core service capabilities are consolidated directly +within the main `appfunctions` artifact. // build.gradle.kts dependencies { @@ -58,13 +66,16 @@ Remove the standalone `appfunctions-service` library from your module build file ### Create a dedicated wrapper service extending `AppFunctionService` -Instead of annotating standalone business logic classes or implementing manual configuration providers, create an abstract service wrapper across your project, for example `BaseAppFunctionService`, extending `AppFunctionService` and annotated with `@AppFunctionServiceEntryPoint`. +Instead of annotating standalone business logic classes or implementing manual +configuration providers, create an abstract service wrapper across your project, +for example `BaseAppFunctionService`, extending `AppFunctionService` and +annotated with `@AppFunctionServiceEntryPoint`. #### Recommended approach using Hilt -Annotate your service with `@AndroidEntryPoint` and inject your data repositories or use cases using standard `@Inject internal lateinit var`: +Annotate your service with `@AndroidEntryPoint` and inject your data +repositories or use cases using standard `@Inject internal lateinit var`: -
```kotlin @RequiresApi(36) @@ -85,16 +96,19 @@ abstract class BaseAppFunctionService : AppFunctionService() { return messageRepository.send(name, endpointValue, messageBody) } } - ```
#### Framework-agnostic approach using alternative dependency injection or a service locator -While Hilt is recommended, many Android applications implement AppFunctions with alternative dependency injection frameworks like Koin, Anvil, or manual Service Locators. Because `AppFunctionService` inherits from Android `android.app.Service` and therefore `Context`, you are able access your application's DI container directly through `applicationContext` in property getters or during service lifecycle execution: +While Hilt is recommended, many Android applications implement AppFunctions with +alternative dependency injection frameworks like Koin, Anvil, or manual Service +Locators. Because `AppFunctionService` inherits from Android +`android.app.Service` and therefore `Context`, you are able access your +application's DI container directly through `applicationContext` in property +getters or during service lifecycle execution: -
```kotlin @RequiresApi(36) @@ -119,7 +133,6 @@ abstract class ServiceLocatorBaseAppFunctionService : AppFunctionService() { return messageRepository.send(name, endpointValue, messageBody) } } - ```
@@ -131,7 +144,10 @@ abstract class ServiceLocatorBaseAppFunctionService : AppFunctionService() { ### Simplify method signatures and decouple context -Remove legacy `AppFunctionContext` parameters from your core methods. When a method requires an Android `Context`, for example when constructing a `PendingIntent`, access `this` directly from your `AppFunctionService` wrapper because the wrapper inherently extends `android.content.Context`. +Remove legacy `AppFunctionContext` parameters from your core methods. When a +method requires an Android `Context`, for example when constructing a +`PendingIntent`, access `this` directly from your `AppFunctionService` wrapper +because the wrapper inherently extends `android.content.Context`. - suspend fun makeCall(appFunctionContext: AppFunctionContext, contactName: String?): PendingIntent + suspend fun makeCall(contactName: String?): PendingIntent @@ -140,7 +156,8 @@ Remove legacy `AppFunctionContext` parameters from your core methods. When a met ### Remove legacy configuration provider -Update your `Application` class by removing `AppFunctionConfiguration.Provider` and its associated builder entry points: +Update your `Application` class by removing +`AppFunctionConfiguration.Provider` and its associated builder entry points: - abstract class BaseChatApplication : Application(), AppFunctionConfiguration.Provider { ... } + abstract class BaseChatApplication : Application() @@ -149,15 +166,21 @@ Update your `Application` class by removing `AppFunctionConfiguration.Provider` ### Avoid redundant abstraction layers -Don't attempt to make an `AppFunction` class or method OS-agnostic---AppFunctions are inherently part of the Android platform integration through the `androidx.appfunctions` package. For architectural cleanliness, use existing application functionality, such as existing repositories, use cases, or domain orchestrators, to execute the behavior within your `@AppFunction` methods rather than creating redundant abstraction layers around the OS service. +Don't attempt to make an `AppFunction` class or method OS-agnostic---AppFunctions +are inherently part of the Android platform integration through the +`androidx.appfunctions` package. For architectural cleanliness, use existing +application functionality, such as existing repositories, use cases, or domain +orchestrators, to execute the behavior within your `@AppFunction` methods rather +than creating redundant abstraction layers around the OS service. *** ** * ** *** ### Consolidate service and metadata manifest declarations -Register the KSP-generated service declaration and `app_metadata` property inside your module manifest, for example in `src/main/AndroidManifest.xml` within the `` tag: +Register the KSP-generated service declaration and `app_metadata` property +inside your module manifest, for example in `src/main/AndroidManifest.xml` +within the `` tag: -
```xml - ```
@@ -187,7 +209,18 @@ Register the KSP-generated service declaration and `app_metadata` property insid ## Verification and troubleshooting -1. **Clean rebuild and deploy** : `bash ./gradlew clean installDebug` -2. **Verify AppSearch discovery / indexing** : Run the following ADB command to confirm the OS successfully discovered and indexed your functions: `bash adb shell cmd app_function list-app-functions` *If your package doesn't appear, confirm that `android.app.appfunctions.v2` matches the exact asset name generated in `assets/`.* +1. **Clean rebuild and deploy** : `bash + ./gradlew clean installDebug` +2. **Verify AppSearch discovery / indexing** : Run the following ADB command to + confirm the OS successfully discovered and indexed your functions: + `bash + adb shell cmd app_function list-app-functions` + *If your package doesn't appear, confirm that `android.app.appfunctions.v2` + matches the exact asset name generated in `assets/`.* -3. **Verify execution using ADB** : `bash adb shell "cmd app_function execute-app-function \ --package com.example.chatapp \ --function 'com.example.chatapp.appfunctions.BaseAppFunctionService#send' \ --parameters '{\"name\": \"Alice\", \"endpointValue\": \"1\", \"messageBody\": \"Hello Alice!\"}'"` \ No newline at end of file +3. **Verify execution using ADB** : + `bash + adb shell "cmd app_function execute-app-function \ + --package com.example.chatapp \ + --function 'com.example.chatapp.appfunctions.BaseAppFunctionService#send' \ + --parameters '{\"name\": \"Alice\", \"endpointValue\": \"1\", \"messageBody\": \"Hello Alice!\"}'"` \ No newline at end of file diff --git a/identity/restore-credentials/SKILL.md b/identity/restore-credentials/SKILL.md new file mode 100644 index 0000000..dcb8e40 --- /dev/null +++ b/identity/restore-credentials/SKILL.md @@ -0,0 +1,340 @@ +--- +name: restore-credentials +description: Provides knowledge and workflows to implement Android's Restore Credentials + feature using the androidx.credentials library. Use this skill to create, sign in + with, and delete restore keys, enabling silent user sign-in on new devices after + a restore. It covers version compatibility, dependencies, server-side prerequisites, + and the complete client-side implementation for creating, retrieving, and clearing + restore keys. +license: Complete terms in LICENSE.txt +metadata: + author: Google LLC + last-updated: '2026-08-21' + keywords: + - Credential Manager + - Restore Credentials + - backup & restore + - backup + - restore + - implementation +--- + +## Fundamentals + +The objective is to implement the **Restore Credentials** feature through the +Android Credential Manager API (`androidx.credentials`). This allows apps that +use or are integrating Credential Manager to silently log users back in when +they restore their app on a new device. Restore Credentials operates +independently of the app's primary authentication method (passwords, passkeys, +federated sign-in) and requires no UI changes to existing sign-in flows. + +### Scope + +**Crucial:** This skill focuses exclusively on the Android client-side +integration. It does **not** implement the server-side cryptographic +validation logic. The developer must be reminded of this and the +[Points to inform the developer about](skill.md) after implementation is done. + +## Implementation Guidelines + +When instructed to implement Restore Credentials on a developer's application, +remember the following: + +1. Before the implementation, you **MUST** read and understand the [Two-Tier + Restoration Architecture](skill.md) and review the [DOs and DON'Ts](skill.md). +2. After the implementation, you **MUST** present the developer with the [Backend Guidelines](skill.md) as a reminder for their backend setup. It is important that you remind the developer that they still have to implement the backend. + +## Two-Tier Restoration Architecture + +To enable a resilient sign-in experience, retrieve credentials through a +**two-tier architecture**: + +1. **Tier 1 (Primary - Background):** Executes automatically during device setup using the app's `BackupAgent.onRestoreFinished()` callback. This provides an invisible restoration before the user opens the app for the first time, allowing background sync and notification delivery. +2. **Tier 2 (Secondary - Foreground):** Runs in the Launcher `Activity.onCreate()` to catch failovers if background restoration didn't complete (example: dropped network, delayed restoration) or if `allowBackup` is disabled. + +If `allowBackup` in the manifest is set to true, implement both. Otherwise, only +implement tier 2 (Foreground Restoration). Do **NOT** change the value of +`allowBackup` in the manifest. + +## DOs and DON'Ts + +**DO:** + +- Do check `AndroidManifest.xml` for the value of allowBackup to determine what you have to implement. +- Do implement a fallback for createCredential: always try calling it first with `isCloudBackupEnabled` set to true. If an `E2eeUnavailableException` is thrown, catch it and retry the call with `isCloudBackupEnabled` set to `false`. +- Do implement a `BackupAgent` (subclass of `android.app.backup.BackupAgent`) if `allowBackup` is true in the manifest. +- Call `clearCredentialState()` when the user signs out. This is a mandatory security measure to log the user out fully. +- Do attempt to get the restore key on the first launch of the app on a new device and also within the `BackupAgent.onRestoreFinished()` callback if your app uses it. +- Do ensure that a restore credential is created even if the user is already logged in. +- Do ensure that the credential retrieval and login in `onRestoreFinished()` is performed synchronously (for example using `runBlocking`). +- Do restore notifications in the `BackupAgent` if your app uses them. (For example capture and send FCM token to backend) +- Do ensure that if you implement mock network requests or stubs, you replace all placeholders with valid, properly formatted JSON payloads for the credential requests. +- Do encapsulate credential creation and retrieval into their own dedicated functions. Because credential creation must be called in multiple places (sign-up, sign-in) and retrieval across multiple tiers (`BackupAgent` and Launcher `Activity`), this prevents code duplication. +- Do remind the developer of the [critical guidelines](skill.md) for implementing the backend once you're done with the implementation. +- Do generate a separate restore key for each application if the organization has multiple apps with different package names, as a restore key is tied to a unique application package name. + +**DON'T:** + +- DON'T change the value of `allowBackup` in `AndroidManifest.xml`. Restore Credentials functionality is not affected by the allowBackup setting, meaning the user will still be automatically logged in when a Restore Credential exists, even if `allowBackup` is false. +- DON'T implement a `BackupAgent` if `allowBackup` is `false` in `AndroidManifest.xml`. +- DON'T assume the credential stored in the `GetCredentialResponse` to be of type `PublicKeyCredential`. It has type `RestoreCredential`. +- DON'T chain `GetRestoreCredentialOption` with any other `CredentialOption` in the construction of a `GetCredentialRequest`. +- DON'T assume `CredentialManager` or the Android system will automatically delete a restore key when a user signs out of the app. You must explicitly call `clearCredentialState` with a `ClearCredentialStateRequest` of `TYPE_CLEAR_RESTORE_CREDENTIAL`. +- DON'T remove any existing calls to `clearCredentialState()`. A `ClearCredentialStateRequest` without a type specified only clears all NON-restore credentials. + +## Implementation Guide + +Implement the Android client-side code by using the following guide. Follow it +**step-by-step** and don't implement any backend functionality, only remind +the user of the [Backend Guidelines](skill.md) once you're done. + +## Version compatibility + +Credential Manager's Restore Credentials works on devices running Android 9 and +higher, Google Play services (GMS) core version 24220000 or higher, and version +1.5.0 or higher of the `androidx.credentials` library. + +## Prerequisites + +Set up a [relying party server](skill.md) similar to the server for [passkeys](skill.md). If +you already have a [server](skill.md) set up to handle authentication with passkeys, +use the same server-side implementation for restore keys. + +> [!NOTE] +> **Note:** While the server-side implementation is the same for passkeys and restore keys, your client-side app can support restore keys without supporting passkeys. Because restore keys work independently of the authentication method in your app (for example, passwords or Sign in with Google), you don't need to make any additional changes to the existing authentication methods in your app's code. + +## Dependencies + +Add the following dependencies to your app module's `build.gradle` file: + +### Kotlin + +```kotlin +dependencies { + implementation("androidx.credentials:credentials:1.7.0-alpha03") + implementation("androidx.credentials:credentials-play-services-auth:1.7.0-alpha03") +} +``` + +### Groovy + +```groovy +dependencies { + implementation "androidx.credentials:credentials:1.7.0-alpha03" + implementation "androidx.credentials:credentials-play-services-auth:1.7.0-alpha03" +} +``` + +Restore Credentials is available from version 1.5.0 and higher of the +androidx.credentials library. However, it's recommended to use the latest stable +versions of the dependencies where possible. + +> [!NOTE] +> **Note:** The Restore Credentials feature works regardless of whether [`allowBackup`](references/android/guide/topics/manifest/application-element.md) is set in the `manifest`. + +## Overview + +1. [**Create a restore key**](skill.md): To create a restore key, complete the following steps: + 1. [**Instantiate Credential Manager**](skill.md): Create a `CredentialManager` object. + 2. [**Get credential creation options from the app server**](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API): Send the client app the details required to create the restore key from your app server. + 3. [**Create the restore key**](https://w3c.github.io/webauthn/#dictdef-publickeycredentialcreationoptionsjson): Create a restore key for the user's account if the user is signed in to your app. + 4. [**Handle the credential creation response**](https://w3c.github.io/webauthn/#dictdef-publickeycredentialrequestoptionsjson): Send the credentials from your client app to your app server for processing, and handle any exceptions. +2. [**Sign in with a restore key**](skill.md): To sign in with a restore key, complete the following steps: + 1. [**Get credential retrieval options from the app server**](skill.md): Send the client app the details required to retrieve the restore key from your app server. + 2. [**Get the restore key**](skill.md): Request the restore key from Credential Manager when the user sets up a new device. This lets the user sign in without additional input. + 3. [**Handle the credential retrieval response**](skill.md): Send the restore key from the client app to the app server to sign in the user. +3. [**Delete a restore key**](skill.md). + +## Create a restore key + +Your app should cover all cases of a user signing in to ensure active users have +a restore key created. Create the restore key in the following scenarios: + +- If the user is signed in and a restore key isn't already created (such as in the `onCreate` method for the main `Activity`). +- When the user is signing in or completing a new account registration flow. + +To optimize performance and avoid the overhead of creating or checking for a +restore credential on every single login, set a `boolean` flag or a credential +creation timestamp in local storage, such as `has_synced_restore_credential`, to +track whether the key has already been created. + +> [!NOTE] +> **Note:** A restore key is tied to an application's unique package name. If your organization's main app and sub-apps have different package names, create a separate restore key for each app. + +### Instantiate Credential Manager + +Use your app's activity context to instantiate a `CredentialManager` object. + + // Use your app or activity context to instantiate a client instance of + // CredentialManager. + private val credentialManager = CredentialManager.create(context) + +### Get credential creation options from your app server + +Use a FIDO-compliant library in your app server to send your client app the +information required to create the restore credential, such as information about +the user, the app, and additional configuration properties. For more information +about the server-side implementation, see [Server-side +guidance](https://developers.google.com/identity/passkeys/developer-guides/server-registration). + +### Create the restore key + +After parsing the public key creation options sent by the server, create a +restore key by wrapping these options in a +[`CreateRestoreCredentialRequest`](https://developer.android.com/reference/androidx/credentials/CreateRestoreCredentialRequest) object and calling the +[`createCredential()`](https://developer.android.com/reference/androidx/credentials/CredentialManager#createCredential(android.content.Context,androidx.credentials.CreateCredentialRequest)) method with the `CredentialManager` object. + + // createRestoreRequest contains the details sent by the server + val response = credentialManager.createCredential(context, createRestoreRequest) + +#### Key points about the code + +- The `CreateRestoreCredentialRequest` object contains the following fields: + + - `requestJson`: The credential creation options sent by the app server in the [Web Authentication API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API) format for [`PublicKeyCredentialCreationOptionsJSON`](https://w3c.github.io/webauthn/#dictdef-publickeycredentialcreationoptionsjson). + - `isCloudBackupEnabled`: `Boolean` field to determine if the restore key + should be backed up to the cloud. By default, this flag is `true`. This + field has these values: + + - `true`: (**Recommended**) This value enables the backup of restore keys to the cloud if the user has Google Backup and end-to-end encryption, such as a screen lock, enabled. + - `false`: This value saves the key locally and not in the cloud. The key is not available on the new device if the user chooses to restore from the cloud. + + > [!CAUTION] + > **Caution:** It is recommended to set `isCloudBackupEnabled` to `true`. If cloud backup is disabled and the user restores from a cloud backup, the call to retrieve the restore key fails. Users who restore your app with a cloud backup don't receive the restore key and are not automatically signed in. + +### Handle the credential creation response + +The Credential Manager API returns a response of type +[`CreateRestoreCredentialResponse`](https://developer.android.com/reference/androidx/credentials/CreateRestoreCredentialResponse). This response holds the public key +credential registration response in [JSON format](https://w3c.github.io/webauthn/#authenticatorattestationresponse). + +Send the public key from your app to the relying party server. This public key +is similar to the public key generated when you create a passkey. The same code +that handles passkey creation on the server can also handle restore key +creation. For more information about the server-side implementation, see [the +guidance for passkeys](references/android/identity/passkeys/create-passkeys.md). + +During the restore key creation process, handle these exceptions: + +- [`CreateRestoreCredentialDomException`](https://developer.android.com/reference/androidx/credentials/exceptions/restorecredential/CreateRestoreCredentialDomException): This exception occurs if `requestJson` is invalid and does not follow the WebAuthn format for [`PublicKeyCredentialCreationOptionsJSON`](https://w3c.github.io/webauthn/#dictdef-publickeycredentialcreationoptionsjson). +- [`E2eeUnavailableException`](https://developer.android.com/reference/androidx/credentials/exceptions/restorecredential/E2eeUnavailableException): This exception occurs if `isCloudBackupEnabled` is `true`, but the user's device does not have data backup or end-to-end encryption, such as a screen lock. + To ensure that Restore Credentials are created in all cases, you must handle the `E2eeUnavailableException` explicitly by calling `createCredential` with `isCloudBackupEnabled` set to `true`. If `E2eeUnavailableException` is thrown, catch and call `createCredential` again with `isCloudBackupEnabled` set to `false`. +- `IllegalArgumentException`: This exception occurs if `createRestoreRequest` is empty or not valid JSON, or if it does not have a valid `user.id` that conforms to the WebAuthn [specifications](https://w3c.github.io/webauthn/#dictdef-publickeycredentialcreationoptionsjson). + +## Sign in with a restore key + +Use Restore Credentials to silently sign in the user during the device setup +process. + +### Get credential retrieval options from the app server + +Send the client app the options required to get the restore key from the server. +For similar passkey guidance for this step, see [Sign in with a passkey](references/android/identity/passkeys/sign-in-with-passkeys.md). +For more information about the server-side implementation, see the [server-side +authentication guide](https://developers.google.com/identity/passkeys/developer-guides/server-authentication#create_credential_request_options). + +### Get the restore key + +To get the restore key on the new device, call the `getCredential()` method on +the `CredentialManager` object. + +It is recommended to fetch the restore key in both of the following scenarios: + +- On the first launch of the app on the device. Credential restoration in this scenario is independent of restoration of the app data. +- If app data backup and restore is enabled, get the restore key immediately after the app data is restored. Use [`BackupAgent`](https://developer.android.com/reference/android/app/backup/BackupAgent) to configure your app's backup and ensure you complete the `getCredential` functionality within the [`onRestoreFinished`](https://developer.android.com/reference/android/app/backup/BackupAgent#onRestoreFinished()) callback. Don't use the `onRestore` method, as it is only called for key-value backups, whereas `onRestoreFinished` is reliably called for any kind of backup restore. This avoids potential delays when users open their new device for the first time and lets users interact with the app without waiting for them to open your app. For example, this lets your app send the user notifications before they open the app for the first time on the new device, which is particularly relevant for messaging or communications apps. + +> [!IMPORTANT] +> **Important:** Notifications aren't automatically restored after the restore credentials are retrieved. If you use Firebase to handle notifications, you must fetch and send the Firebase Cloud Messaging (FCM) token to the backend to successfully resume background messaging and notifications. + + // Fetch the options required to get the restore key + val authenticationJson = fetchAuthenticationJson() + + // Create the GetRestoreCredentialRequest object + val options = GetRestoreCredentialOption(authenticationJson) + val getRequest = GetCredentialRequest(listOf(options)) + + val response = credentialManager.getCredential(context, getRequest) + + // Type-check and extract the restore credential + val credential = response.credential as RestoreCredential + +The credential manager APIs return a response of type +[`GetCredentialResponse`](https://developer.android.com/reference/android/credentials/GetCredentialResponse). The credential contained in this response is +explicitly of type `RestoreCredential`, which holds the public key. + +### Handle the sign-in response + +Send the public key from the app to the relying party server, which can then be +used to sign in the user. On the server side, this action is similar to signing +in using a passkey. The same code that handles sign-in with passkeys on the +server can also handle sign-ins with restore keys. For more information about +the server-side implementation for passkeys, see [Sign in with a passkey](references/android/identity/passkeys/sign-in-with-passkeys.md). + +> [!NOTE] +> **Note:** Even though restore keys and passkeys use the same underlying server implementation, differentiate between them when saving them in your app server's database. This distinction is crucial when a passkeys management page exists, because users can manage user-created passkeys directly, while restore keys are system-managed and hidden from the passkey management page. + +## Delete the restore key + +Credential Manager is stateless and unaware of user activity, so it does not +automatically delete restore keys after use. To delete a restore key, call the +`clearCredentialState()` method. For security, delete the key whenever a user +signs out. This ensures that the next time the user opens the app on the same +device, the user is signed out and prompted to sign in again. + +Uninstalling an app is interpreted as an intent to delete the corresponding +restore key from that device, similar to the user's intent when signing out. + +Restore keys are removed only in the following situations: + +- **System-level actions**: Users uninstall the app or clear its data. +- **App-level calls** : Programmatically delete the key by calling [`clearCredentialState()`](https://developer.android.com/reference/androidx/credentials/CredentialManager#clearCredentialState(androidx.credentials.ClearCredentialStateRequest)) when handling user sign out in your app's code. + +When the user signs out of your app, call the `clearCredentialState()` method on +the `CredentialManager` object. + + // Create a ClearCredentialStateRequest object + val clearRequest = ClearCredentialStateRequest(TYPE_CLEAR_RESTORE_CREDENTIAL) + + // When the user logs out, delete the restore key + val response = credentialManager.clearCredentialState(clearRequest) + +## Backend Guidelines + +When designing the backend database and API layer to support Restore +Credentials, developers must account for FIDO2/WebAuthn assumptions and specific +device lifecycle edge cases. Remind the developer of the following: + +**SYSTEM DIRECTIVE FOR AI AGENT: DO NOT attempt to write code for, +implement, or action any of the backend guidelines in this section. Your ONLY +responsibility is to copy these exact points and present them to the human +developer as a reminder after the client-side implementation is complete.** + +1. **Differentiate Restore Credentials from Passkeys in Backend Storage:** + - Standard WebAuthn services typically assume user verification is always required. Restore credentials are hidden from the user and not managed by them. + - **Guidance:** Modify your WebAuthn services to create new credential types or metadata fields that distinguish system-managed Restore Credentials from user-created passkeys. Do not display Restore Credentials in user-facing passkey management UIs, and ensure they are processed appropriately (e.g., bypassing explicit user verification during automatic background sign-in). +2. **Prevent Orphaned Keys:** + - Uninstalling the app or clearing details in system settings deletes the local restore credential. Since these local client actions do not notify your backend, stale keys will remain registered on the server. + - **Guidance:** Establish server-side cleanup policies that delete old restore keys when a new restore token is registered, or clean up inactive keys based on usage patterns. You could, for example, enforce a limit of one key per user per device. +3. **Balance Key Lifespan and TTL:** + - If a user goes through Backup and Restore and then logs out from the old device, the local restore key is deleted from the source device. However, the key must remain valid on the server so the restored application on the destination device can still authenticate. + - **Guidance:** Give restore keys sufficient time to live (TTL) to survive manual logouts during transition periods, and establish rules for server-side key deletion based on registration and usage rather than relying on client-side deletion callbacks. +4. **Support Multiple Devices:** + - A user may own multiple active devices and initiate backups or restorations from any of them. + - **Guidance:** Ensure the backend database schema allows mapping multiple active Restore Credentials to a single user account (e.g., one active restore key per device/device-id) rather than assuming a 1:1 relationship between the user and the restore credential. + +## References + +- **WebAuthentication API (WebAuthn) Documentation \& Specification** + *When to use:* Use these resources ONLY if you need to inspect or debug the + strict JSON schema requirements for FIDO2/WebAuthn, specifically when + generating mock data or formatting the `requestJson` + (`PublicKeyCredentialCreationOptionsJSON`) and `authenticationJson` payloads. + +- [MDN Web Authentication API Documentation](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API): Mozilla Developer Network + guide and reference for WebAuthn APIs + +- [W3C `PublicKeyCredentialCreationOptionsJSON`](https://w3c.github.io/webauthn/#dictdef-publickeycredentialcreationoptionsjson): Data structure definition + for WebAuthn credential creation requests in JSON format. + +- [W3C `PublicKeyCredentialRequestOptionsJSON`](https://w3c.github.io/webauthn/#dictdef-publickeycredentialrequestoptionsjson): Data structure definition + for WebAuthn authentication or assertion requests in JSON format. diff --git a/identity/restore-credentials/references/android/guide/topics/manifest/application-element.md b/identity/restore-credentials/references/android/guide/topics/manifest/application-element.md new file mode 100644 index 0000000..638053a --- /dev/null +++ b/identity/restore-credentials/references/android/guide/topics/manifest/application-element.md @@ -0,0 +1,832 @@ +syntax: +: + + ```xml + + . . . + + ``` + +contained in: +: `https://developer.android.com/guide/topics/manifest/manifest-element` + +can contain: +: `https://developer.android.com/guide/topics/manifest/activity-element` + + `https://developer.android.com/guide/topics/manifest/activity-alias-element` + + `https://developer.android.com/guide/topics/manifest/meta-data-element` + + `https://developer.android.com/guide/topics/manifest/service-element` + + `https://developer.android.com/guide/topics/manifest/receiver-element` + + `https://developer.android.com/guide/topics/manifest/profileable-element` + + `https://developer.android.com/guide/topics/manifest/provider-element` + + `https://developer.android.com/guide/topics/manifest/uses-library-element` + + `https://developer.android.com/guide/topics/manifest/uses-native-library-element` + + `https://developer.android.com/guide/topics/manifest/property-element` + +description: + +: The declaration of the application. This element contains subelements + that declare each of the application's components and has attributes + that can affect all the components. + + Many of these attributes, such as + `icon`, `label`, `permission`, `process`, + `taskAffinity`, and `allowTaskReparenting`, set default values + for corresponding attributes of the component elements. Others, such as + `debuggable`, `enabled`, `description`, and + `allowClearUserData`, set values for the application as a whole and + aren't overridden by the components. + +attributes +: + + `android:allowTaskReparenting` + : Whether activities that the application defines can move from + the task that started them to the task they have an affinity for when that task + is next brought to the front. It's `"true"` if they can move, and + `"false"` if they must remain with the task where they started. + The default value is `"false"`. + + + The + `https://developer.android.com/guide/topics/manifest/activity-element` + element has its own + `https://developer.android.com/guide/topics/manifest/activity-element#reparent` + attribute that can override the value set here. + + `android:allowBackup` + + : Whether to let the application participate in the backup + and restore infrastructure. If this attribute is set to `"false"`, no + backup or restore of the application is ever performed, disabling all cloud backups + and device-to-device (D2D) transfers, even by a full-system backup that otherwise + causes all application data to save using `adb`. The default value of this + attribute is `"true"`. If your app processes sensitive data, you can keep + this attribute set to `"true"` and configure + [`android:dataExtractionRules`](https://developer.android.com/guide/topics/manifest/application-element#dataExtractionRules) to safely + exclude sensitive keys or temporary caches while preserving user preferences and + continuity. + + **Note:**For apps targeting Android 12 (API level 31) or higher, this behavior + varies. On devices from some device manufacturers, you can't disable device-to-device migration + of your app's files. + + However, you can disable cloud-based backup and restore of your app's files by setting + this attribute to `"false"`, even if your app targets Android 12 (API level 31) or + higher. + + + For more information, see the + [backup and restore](https://developer.android.com/about/versions/12/behavior-changes-12#backup-restore) section + of the page that describes behavior changes for apps targeting Android 12 (API level 31) or + higher. + + `android:allowClearUserData` + + : Whether to let the application reset user data. This data includes + flags, such as whether the user has seen introductory tooltips, as + well as user-customizable settings and preferences. The default value of this + attribute is `"true"`. + + **Note:** Only apps that are part of the system image can + declare this attribute explicitly. Third-party apps can't include this + attribute in their manifest files. + + For more information, see [Data backup overview](https://developer.android.com/guide/topics/data/backup). + + `android:allowCrossUidActivitySwitchFromBelow` + + : Specifies whether activities from other applications in the same task + can be launched on top of this application. If set to `"false"`, + activity launches that would replace this application in the user's view + are blocked. + + The default value is `"true"`. + + For more information, see the guide to + [Secure Background Activity Launches](https://developer.android.com/guide/components/activities/secure-bal). + + `android:allowNativeHeapPointerTagging` + + : Whether the app enables the Heap pointer tagging feature. The default value of + this attribute is `"true"`. + + **Note:** Disabling this feature **doesn't** address the + underlying code health issue. Future hardware devices might not support this manifest tag. + + For more information, see [Tagged Pointers](https://source.android.com/devices/tech/debug/tagged-pointers). + + `android:appCategory` + + : Declares the category of this app. Categories are used to cluster multiple apps + together into meaningful groups, such as when summarizing battery, network, or + disk usage. Only define this value for apps that fit well into one of + the specific categories. + + Must be one of the following constant values. + + | Value | Description | + |---|---| + | `accessibility` | Apps that are primarily accessibility apps, such as screen-readers. | + | `audio` | Apps that primarily work with audio or music, such as music players. | + | `game` | Apps that are primarily games. | + | `image` | Apps that primarily work with images or photos, such as camera or gallery apps. | + | `maps` | Apps that are primarily map apps, such as navigation apps. | + | `news` | Apps that are primarily news apps, such as newspapers, magazines, or sports apps. | + | `productivity` | Apps that are primarily productivity apps, such as cloud storage or workplace apps. | + | `social` | Apps that are primarily social apps, such as messaging, communication, email, or social network apps. | + | `video` | Apps that primarily work with video or movies, such as streaming video apps. | + + + `android:backupAgent` + : The name of the class that implements the application's backup agent, + a subclass of `https://developer.android.com/reference/android/app/backup/BackupAgent`. The attribute value is + a fully qualified class name, such as `"com.example.project.MyBackupAgent"`. + However, as a shorthand, if the first character of the name is a period, + for example, `".MyBackupAgent"`, it is appended to the + package name specified in the + `https://developer.android.com/guide/topics/manifest/manifest-element` + element. + + + There is no default. The name must be specific. + + `android:backupInForeground` + : Indicates that + [Auto Backup](https://developer.android.com/guide/topics/data/autobackup) operations + can be performed on this app even if the app is in a foreground-equivalent + state. The system shuts down an app during auto backup operation, + so use this attribute with caution. Setting this flag to `"true"` can impact app + behavior while the app is active. + + The default value is `"false"`, which means that the OS avoids + backing up the app while it's running in the foreground, such as a music app + that is actively playing music using a service in the + `https://developer.android.com/reference/android/app/Service#startForeground(int, android.app.Notification)` state. + + `android:banner` + : A [drawable resource](https://developer.android.com/guide/topics/resources/drawable-resource) + providing an extended graphical banner for its associated item. Use with the + `` tag to supply a default banner for all application activities or with the + [``](https://developer.android.com/guide/topics/manifest/activity-element) + tag to supply a banner for a specific activity. + + The system uses the banner to represent an app in + the Android TV home screen. Therefore, only specify this for applications with an activity that handles the + `https://developer.android.com/reference/android/content/Intent#CATEGORY_LEANBACK_LAUNCHER` intent. + + + This attribute is set as a reference to a drawable resource containing + the image, for example `"@drawable/banner"`. There is no default banner. + + + For more information, see [Provide a home screen banner](https://developer.android.com/training/tv/start/start#banner). + + `android:dataExtractionRules` + + : + Applications can set this attribute to an XML resource where they specify the + rules determining which files and directories can be copied from the device as part of backup or + transfer operations. + + + For information about the format of the XML file, see [Backup and restore](https://developer.android.com/about/versions/12/behavior-changes-12#backup-restore). + + `android:debuggable` + : Whether the application can be debugged, even when running + on a device in user mode. It's `"true"` if it can be and `"false"` + if not. The default value is `"false"`. + + `android:description` + : User-readable text about the application, which is longer and more descriptive than the application label. + The value is set as a reference to a string resource. Unlike the label, it can't be a raw string. + There is no default value. + + `android:enabled` + : Whether the Android system can instantiate components of + the application. It's `"true"` if it can and `"false"` + if not. If the value is `"true"`, each component's + `enabled` attribute determines whether that component is enabled. + If the value is `"false"`, it overrides the + component-specific values, and all components are disabled. + + + The default value is `"true"`. + + `android:enableOnBackInvokedCallback` + + : This flag lets you opt out of predictive system animations at the app level. + + + Set `android:enableOnBackInvokedCallback=false` to turn off predictive back animations at the app level + and instruct the system to ignore calls to the `OnBackInvokedCallback` platform API. + + `android:extractNativeLibs` + : This attribute indicates whether the package installer extracts native libraries from the + APK to the file system. If set to `"false"`, your native libraries are stored + uncompressed in the APK. Although your APK might be larger, your application loads faster + because the libraries load directly from the APK at runtime. + + + The default value of `extractNativeLibs` depends on `minSdkVersion` and the + version of AGP you're using. In most cases, the default behavior is probably what you want, and + you don't have to set this attribute explicitly. + + `android:forceQueryable` + + : Specifies whether this application is visible to all other applications + on the device, regardless of what those other applications declare in the + `` tags in their manifests. + + The default value is `"false"`. + + For more information, see the guide on + [automatic package visibility filtering](https://developer.android.com/training/package-visibility/automatic). + + `android:fullBackupContent` + : This attribute points to an XML file that contains full backup rules for + [Auto Backup](https://developer.android.com/guide/topics/data/autobackup). + These rules determine what files get backed up. For more information, see the + [XML config syntax](https://developer.android.com/guide/topics/data/autobackup#XMLSyntax) + for Auto Backup. + + + This attribute is optional. If it is not specified, by default, Auto Backup + includes most of your app's files. For more information, see + [Files that are backed up](https://developer.android.com/guide/topics/data/autobackup#Files). + + `android:fullBackupOnly` + : This attribute indicates whether to use + [Auto Backup](https://developer.android.com/guide/topics/data/autobackup) on devices + where it is available. If set to `"true"`, then your app performs + Auto Backup when installed on a device running Android 6.0 (API level 23) or + higher. On older devices, your app ignores this attribute and performs + [key/value backups](https://developer.android.com/guide/topics/data/keyvaluebackup). + + + The default value is `"false"`. + + `android:gwpAsanMode` + : This attribute indicates whether to use + [GWP-ASan](https://developer.android.com/ndk/guides/gwp-asan), a native memory allocator feature that helps + find use-after-free and heap-buffer-overflow bugs. + + + The default value is `"never"`. + + `android:hasCode` + : Whether the application contains any DEX code---that is, code using the + Kotlin or Java programming language. + It's `"true"` if it does and `"false"` if not. When the + value is `"false"`, the system doesn't try to load any application + code when launching components. The default value is `"true"`. + + + If the application includes native (C/C++) code, but no DEX code, this should + be set to `"false"`. If set to `"true"` when the APK + contains no DEX code, the app may fail to load. + + + **This property must account for code included in the application by + dependencies.** If the application depends on an AAR that uses + Java/Kotlin code, or directly on a JAR, `app:hasCode` must be + `"true"`, or omitted as that is the default. + + + For example, your app might support + [Play feature delivery](https://developer.android.com/platform/technology/app-bundle) and include feature + modules that don't generate any DEX files, which is bytecode optimized for the Android + platform. If so, you need to set this property to `"false"` in the module's manifest + file to avoid runtime errors. + + `android:hasFragileUserData` + : Whether to show the user a prompt to + keep the app's data when the user uninstalls the app. The default value is `"false"`. + + `android:hardwareAccelerated` + : Whether hardware-accelerated rendering is enabled for all + activities and views in this application. It's `"true"` if it is + enabled and `"false"` if not. The default value is `"true"` if you set + either [`minSdkVersion`](https://developer.android.com/guide/topics/manifest/uses-sdk-element#min) + or [`targetSdkVersion`](https://developer.android.com/guide/topics/manifest/uses-sdk-element#target) + to `"14"` or higher. Otherwise, it's `"false"`. + + Starting from Android 3.0 (API level 11), a hardware-accelerated OpenGL renderer is + available to applications to improve performance for many common 2D graphics + operations. When the hardware-accelerated renderer is enabled, most operations + in Canvas, Paint, Xfermode, ColorFilter, Shader, and Camera are accelerated. + + + This results in smoother animations, smoother scrolling, and improved + responsiveness overall, even for applications that don't explicitly make use + the framework's OpenGL libraries. + + + Not all of the OpenGL 2D operations are accelerated. If you enable + the hardware-accelerated renderer, test your application so that it can + make use of the renderer without errors. + + + For more information, read the + [Hardware acceleration](https://developer.android.com/guide/topics/graphics/hardware-accel) + guide. + + `android:icon` + : An icon for the application as whole and the default icon for + each of the application's components. See the individual + `icon` attributes for the + `https://developer.android.com/guide/topics/manifest/activity-element`, + `https://developer.android.com/guide/topics/manifest/activity-alias-element`, + `https://developer.android.com/guide/topics/manifest/service-element`, + `https://developer.android.com/guide/topics/manifest/receiver-element`, and + `https://developer.android.com/guide/topics/manifest/provider-element` elements. + + + This attribute is set as a reference to a drawable resource containing + the image, such as `"@drawable/icon"`. There is no default icon. + + `android:intentMatchingFlags` + + : + Use this attribute to fine-tune how the system matches incoming intents to app + components. By default, no special matching rules are applied. + + + This attribute can be specified on the `` tag + as well as on component tags, including ``, + ``, ``, + ``, and ``. The value + set on a component overrides the value set on the + `` tag. + + + The value must be one or more of the following flags, separated by '`|`': + + | Flag | Description | + |---|---| + | `none` | Disables all special matching rules for incoming intents. When specifying multiple flags, conflicting values are resolved by giving precedence to the `none` flag. | + | `enforceIntentFilter` | Enforces stricter matching for incoming intents: - Explicit intents must match the target component's intent filter. - Intents without an action don't match any intent filter. | + | `allowNullAction` | Relaxes the matching rules to allow intents without an action to match. This flag is used in conjunction with `enforceIntentFilter` to achieve the following behavior: - Explicit intents must match the target component's intent filter. - Intents without an action are allowed to match any intent filter. | + + For more information, see the + [Safer Intents](https://developer.android.com/about/versions/16/behavior-changes-16#safer-intents) + section in the Android 16 (API level 36) behavior changes. + + `android:isGame` + : Whether the application is a game. The system might group together applications classified + as games or display them separately from other applications. The default is `"false"`. + + `android:isMonitoringTool` + + : Indicates that this application is designed to monitor other individuals. + + **Note:** If an app declares this attribute in its manifest, the developer must + follow the + [Stalkerware](https://support.google.com/googleplay/android-developer/answer/9888380#commercial-spyware) + policy to publish the app to Google Play. + + There is no default value. The developer must specify one of the following values: + + | Value | Description | + |---|---| + | `"parental_control"` | App caters to parental control and is specifically targeted at parents who want to keep their kids safe. | + | `"enterprise_management"` | App caters to enterprises that want to manage and track devices given to employees. | + | `"other"` | App caters to a use case not otherwise specified in this table. | + + `android:killAfterRestore` + + : Whether the application terminates after its + settings have been restored during a full-system restore operation. + Single-package restore operations never cause the application to + shut down. Full-system restore operations typically only occur once, + when the phone is first set up. Third-party applications don't normally + need to use this attribute. + + The default is `"true"`, which means that after the application + finishes processing its data during a full-system restore, it terminates. + + `android:largeHeap` + + : Whether the application's processes are created with a large Dalvik heap. This applies to + all processes created for the application. It only applies to the first application loaded into a + process. If you're using a shared user ID to let multiple applications use a process, they all + must use this option consistently to avoid unpredictable results. + + Most apps don't need this and instead focus on reducing their overall memory usage for + improved performance. Enabling this also doesn't guarantee a fixed increase in available memory, + because some devices are constrained by their total available memory. + + To query the available memory size at runtime, use the methods `https://developer.android.com/reference/android/app/ActivityManager#getMemoryClass()` or `https://developer.android.com/reference/android/app/ActivityManager#getLargeMemoryClass()`. + + `android:label` + : A user-readable label for the application as a whole and a default + label for each of the application's components. See the individual + `label` attributes for the + `https://developer.android.com/guide/topics/manifest/activity-element`, + `https://developer.android.com/guide/topics/manifest/activity-alias-element`, + `https://developer.android.com/guide/topics/manifest/service-element`, + `https://developer.android.com/guide/topics/manifest/receiver-element`, and + `https://developer.android.com/guide/topics/manifest/provider-element` elements. + + + The label is set as a reference to a string resource, so that + it can be localized like other strings in the user interface. + However, as a convenience while you're developing the application, + it can also be set as a raw string. + + `android:localeConfig` + + : A reference to an XML resource that specifies the list of locales + supported by the application. This is used by the system to support per-app + language preferences. + + For more information, see the guide on + [per-app language preferences](https://developer.android.com/guide/topics/resources/app-languages). + + `android:logo` + : A logo for the application as whole and the default logo for activities. + This attribute is set as a reference to a drawable resource containing + the image, such as `"@drawable/logo"`. There is no default logo. + + `android:manageSpaceActivity` + : The fully qualified name of an `Activity` subclass that the system + launches to let users manage the memory occupied by the application + on the device. The activity is also declared with an + `https://developer.android.com/guide/topics/manifest/activity-element` element. + + `android:name` + : The fully qualified name of an `https://developer.android.com/reference/android/app/Application` + subclass implemented for the application. When the application process + is started, this class is instantiated before any of the application's + components. + + + The subclass is optional. Most applications don't need one. + In the absence of a subclass, Android uses an instance of the base + `Application` class. + + `android:networkSecurityConfig` + + : Specifies the name of the XML file that contains your application's + [Network security + configuration](https://developer.android.com/training/articles/security-config). The value is a reference to the XML resource file + containing the configuration. + + This attribute was added in API level 24. + + `android:pageSizeCompat` + + : Overrides the user or platform compatibility settings for 16 KB page + sizes, which lets you force page-agnostic compatibility mode on or off for + this application. + + For more information, see the guide on + [supporting 16 KB page sizes](https://developer.android.com/guide/practices/page-sizes). + + `android:permission` + : The name of a permission that clients need in order to interact + with the application. This attribute is a convenient way to set a + permission that applies to all of the application's components. It is + overwritten by setting the `permission` attributes of individual + components. + + + For more information about permissions, see the + [Permissions](https://developer.android.com/guide/topics/manifest/manifest-intro#perms) + section in the app manifest overview and + [Security tips](https://developer.android.com/guide/topics/security/security). + + `android:persistent` + : Whether the application remains running at all times. It's + `"true"` if it does and `"false"` if not. The default value + is `"false"`. Applications don't normally set this flag. + Persistence mode is intended only for certain system applications. + + `android:process` + : The name of a process where all components of the application run. + Each component can override this default by setting its own `process` + attribute. + + + By default, Android creates a process for an application when the first + of its components needs to run. All components then run in that process. + The name of the default process matches the package name set by the + `https://developer.android.com/guide/topics/manifest/manifest-element` element. + + + By setting this attribute to a process name that's shared with another + application, you can arrange for components of both applications to run in + the same process, but only if the two applications also share a + user ID and are signed with the same certificate. + + + If the name assigned to this attribute begins with a colon (`:`), a new + process, private to the application, is created when it's needed. + If the process name begins with a lowercase character, a global process + of that name is created. A global process can be shared with other + applications, reducing resource usage. + + `android:restoreAnyVersion` + : Indicates that the application is prepared to attempt a restore of any + backed-up data set, even if the backup was stored by a newer version + of the application than is currently installed on the device. Setting + this attribute to `"true"` lets the Backup Manager + - `$1` + attempt a restore even when a version mismatch suggests that the data is incompatible. *Use with caution!* + - The default value of this attribute is `"false"`. + + `android:requestLegacyExternalStorage` + + : Whether the application wants to opt out of + [scoped storage](https://developer.android.com/training/data-storage/files/external-scoped). + + **Note:** Depending on changes related to policy or app + compatibility, the system might not honor this opt-out request. + + `android:requiredAccountType` + : Specifies the account type required by the application to function. + If your app requires an `https://developer.android.com/reference/android/accounts/Account`, the value for this attribute must + correspond to the account authenticator + type used by your app, as defined by `https://developer.android.com/reference/android/accounts/AuthenticatorDescription`, + such as `"com.google"`. + + The default value is null and indicates that the application + can work *without* any accounts. + + + Because restricted profiles + can't add accounts, specifying this attribute makes your app + unavailable from a restricted profile unless you also declare + [`android:restrictedAccountType`](https://developer.android.com/guide/topics/manifest/application-element#restrictedAccountType) with + the same value. + + + **Caution:** + If the account data might reveal personally identifiable information, it's important + that you declare this attribute and leave [`android:restrictedAccountType`](https://developer.android.com/guide/topics/manifest/application-element#restrictedAccountType) null, so that restricted profiles cannot use + your app to access personal information that belongs to the owner user. + + + This attribute was added in API level 18. + + `android:resizeableActivity` + + : + Specifies whether the app supports [multi-window mode](https://developer.android.com/guide/topics/ui/multi-window). + + > [!WARNING] + > **Warning:** To improve the layout of apps on form factors with smallest width \>= 600dp, the system ignores this attribute for apps that target Android 16 (API level 36). Your app can opt out of the Android 16 behavior, but the opt out will be eliminated in a future release. See [Device compatibility mode](https://developer.android.com/guide/practices/device-compatibility-mode#android_16). + + + You can set this attribute in either the [``](https://developer.android.com/guide/topics/manifest/activity-element) + or `` element. + + + If you set this attribute to `"true"`, the user can launch the activity in + split-screen and free-form modes. If you set the attribute to `"false"`, the app + can't be tested or optimized for a multi-window environment. The system can + still put the activity in multi-window mode with compatibility mode applied. + + + Setting this attribute to `"true"` doesn't guarantee that there are no + other apps in multi-window mode visible on screen, such as picture-in-picture, or + on other displays. Therefore, setting this flag doesn't mean that your app + has exclusive resource access. + + + For apps targeting API level 24 or higher, the default is `"true"`. + + + If your app targets API level 31 or higher, this attribute works differently on small and large + screens: + + - **Large screens (sw \>= 600dp):** all apps support multi-window mode. The attribute indicates whether an app can be resized, not whether the app supports multi-window mode. If `resizeableActivity="false"`, the app is put into compatibility mode when necessary to conform to display dimensions. + - **Small screens (sw \< 600dp):** if `resizeableActivity="true"` and the minimum width and minimum height of the main activity are within the multi-window requirements, the app supports multi-window mode. If `resizeableActivity="false"`, the app doesn't support multi-window mode regardless of the activity minimum width and height. + + > [!NOTE] + > **Note:** + > + > - Device manufacturers can override the API level 31 behavior to improve the layout of apps. + > - On devices with Android 16 (API level 36) or higher installed, virtual device owners (select trusted and privileged apps) can configure devices they manage to override (ignore) this attribute to improve app layout. See also [Companion app + > streaming](https://source.android.com/docs/core/permissions/app-streaming). + > + > See [Device compatibility mode](https://developer.android.com/guide/practices/device-compatibility-mode). + + + A task's root activity value is applied to all additional activities launched in the task. That is, if the root + activity of a task is resizable, then the system treats all other activities in the task as resizable. If the root + activity isn't resizable, the other activities in the task aren't resizable. + + + This attribute was added in API level 24. + + `android:restrictedAccountType` + : Specifies the account type required by this application and indicates that restricted profiles + can access such accounts that belong to the owner user. If your app requires an + `https://developer.android.com/reference/android/accounts/Account` and restricted profiles *can* + access the primary user's accounts, the value for this attribute must + correspond to the account authenticator type used by your app, as + defined by `https://developer.android.com/reference/android/accounts/AuthenticatorDescription`, such as `"com.google"`. + + The default value is null and indicates that the application can work *without* any + accounts. + + + **Caution:** + Specifying this attribute lets restricted profiles use your + app with accounts that belong to the owner user, which might reveal personally identifiable + information. If the account might reveal personal details, *don't* + use this attribute. Instead, declare the [`android:requiredAccountType`](https://developer.android.com/guide/topics/manifest/application-element#requiredAccountType) attribute + to make your app unavailable to restricted profiles. + + + This attribute was added in API level 18. + + `android:supportsRtl` + + : Declares whether your application is willing to support right-to-left (RTL) layouts. + + If this is set to `"true"` and + [`targetSdkVersion`](https://developer.android.com/guide/topics/manifest/uses-sdk-element#target) + is set to 17 or higher, various RTL APIs are + activated and used by the system so your app can display RTL layouts. + If this is set to `"false"` or if `targetSdkVersion` is set to 16 or lower, the RTL APIs are ignored + or have no effect, and your app behaves the same regardless of the layout + direction associated to the user's locale choice. That is, your layouts are always left-to-right. + + The default value of this attribute is `"false"`. + + This attribute was added in API level 17. + + `android:taskAffinity` + : An affinity name that applies to all activities within the application, + except for those that set a different affinity with their own + `https://developer.android.com/guide/topics/manifest/activity-element#aff` + attributes. See that attribute for more information. + + + By default, all activities within an application share the same + affinity. The name of that affinity is the same as the package name + set by the + `https://developer.android.com/guide/topics/manifest/manifest-element` element. + + `android:testOnly` + : Indicates whether this application is only for testing purposes. For example, + it might expose functionality or data outside of itself that can cause a security + hole, but be useful for testing. This kind of APK only installs + through `https://developer.android.com/studio/command-line/adb`. You can't publish it to Google Play. + + Android Studio automatically adds this attribute when you click **Run** + ![](https://developer.android.com/static/studio/images/buttons/toolbar-run.png). + + `android:theme` + : A reference to a style resource defining a default theme for all + activities in the application. Individual activities can override + the default by setting their own `https://developer.android.com/guide/topics/manifest/activity-element#theme` + attributes. For more information, see [Styles and themes](https://developer.android.com/guide/topics/ui/themes). + + `android:uiOptions` + : Extra options for an activity's UI. Must be one of the following values: + + | Value | Description | + |---|---| + | `"none"` | No extra UI options. This is the default. | + | `"splitActionBarWhenNarrow"` | Adds a bar at the bottom of the screen to display action items in the *app bar* , also known as the *action bar*, when constrained for horizontal space, such as when in portrait mode on a handset. Instead of a small number of action items appearing in the app bar at the top of the screen, the app bar splits into the top navigation section and the bottom bar for action items. This means a reasonable amount of space is available for the action items and for the navigation and title elements at the top. Menu items aren't split across the two bars. They always appear together. | + + + For more information about the app bar, see [Add the app bar](https://developer.android.com/training/appbar). + + This attribute was added in API level 14. + + `android:usesCleartextTraffic` + : Indicates whether the app intends to use cleartext network traffic, such as cleartext HTTP. + The default value for apps that target API level 27 or lower is `"true"`. Apps that + target API level 28 or higher default to `"false"`. + + **Note:** + This attribute is getting deprecated and will be ignored for apps targeting API levels 38 and above. + Specify a [Network Security Configuration](https://developer.android.com/training/articles/security-config#CleartextTraffic) to control cleartext traffic for API levels 24 and above. + If your app targets API levels 23 and below, you must specify `android:usesCleartextTraffic` in addition to a Network Security Config. + + + When the attribute is set to `"false"`, platform components, for example, HTTP and FTP + stacks, `https://developer.android.com/reference/android/app/DownloadManager`, and + `https://developer.android.com/reference/android/media/MediaPlayer`, refuse the + app's requests to use cleartext traffic. + + Third-party libraries are strongly encouraged to honor this + setting as well. The key reason for avoiding cleartext traffic is the lack of confidentiality, + authenticity, and protections against tampering. A network attacker can eavesdrop on transmitted + data and also modify it without being detected. + + + This flag is honored on a best-effort basis because it's impossible to prevent all cleartext + traffic from Android applications given the level of access provided to them. For example, there's + no expectation that the `https://developer.android.com/reference/java/net/Socket` API honors + this flag, because it can't determine whether its traffic is in cleartext. + + However, most + network traffic from applications is handled by higher-level network stacks and components, which can + honor this flag by either reading it from + `https://developer.android.com/reference/android/content/pm/ApplicationInfo#flags` + or + `https://developer.android.com/reference/android/security/NetworkSecurityPolicy#isCleartextTrafficPermitted()`. + + + **Note:** + `https://developer.android.com/reference/android/webkit/WebView` honors this attribute for + applications targeting API level 26 and higher. + + + During app development, StrictMode can be used to identify any cleartext traffic from the app. + For more information, see + `https://developer.android.com/reference/android/os/StrictMode.VmPolicy.Builder#detectCleartextNetwork()`. + + + This attribute was added in API level 23. + + + This flag is ignored on Android 7.0 (API level 24) and above if an Android Network Security + Config is present. + + `android:vmSafeMode` + : Indicates whether the app wants the virtual machine (VM) to operate + in safe mode. The default value is `"false"`. + + This attribute was added in API level 8, where a value of `"true"` + disabled the Dalvik just-in-time (JIT) compiler. + + + This attribute was adapted in API level 22, where a value of `"true"` + disabled the ART ahead-of-time (AOT) compiler. + +introduced in: +: API level 1 + +see also: +: `https://developer.android.com/guide/topics/manifest/activity-element` + + `https://developer.android.com/guide/topics/manifest/service-element` + + `https://developer.android.com/guide/topics/manifest/receiver-element` + + `https://developer.android.com/guide/topics/manifest/provider-element` \ No newline at end of file diff --git a/identity/restore-credentials/references/android/identity/passkeys/create-passkeys.md b/identity/restore-credentials/references/android/identity/passkeys/create-passkeys.md new file mode 100644 index 0000000..089cc21 --- /dev/null +++ b/identity/restore-credentials/references/android/identity/passkeys/create-passkeys.md @@ -0,0 +1,327 @@ +Before your users can authenticate with passkeys, your app must first register +or create the passkey for their account. + +To create the passkey, obtain details required to create the passkey from your +app server, and then call the Credential Manager API, which returns a public and +private key pair. The returned private key is stored in a credential provider, +such as Google Password Manager, as a passkey. The public key is stored on your +app server. + +> [!TIP] +> **Tip:** While designing authentication flows with passkeys, make sure that you follow the [UX guidelines for passkeys](https://developer.android.com/design/ui/mobile/guides/patterns/passkeys). + +![Passkeys are stored in a credential provider and public keys are stored on the app server](https://developer.android.com/static/identity/passkeys/images/create-passkeys.png) **Figure 1:**Creation of passkeys + +## Prerequisites + +Make sure that you have set up [Digital Asset Links](https://developer.android.com/identity/credential-manager/prerequisites) and that you target +devices running Android 9 (API level 28) or higher. + +## Overview + +This guide focuses on the changes required in your [relying party client app](https://developer.android.com/identity/credential-manager#authentication-terminology) +to create a passkey, and gives a brief overview of the [relying party app +server](https://developer.android.com/identity/credential-manager#authentication-terminology) implementation. To learn more about the server-side integration, see +[Server-side passkey registration](https://developers.google.com/identity/passkeys/developer-guides/server-registration). + +1. [**Add dependencies to your app**](https://developer.android.com/identity/passkeys/create-passkeys#add-dependencies): Add the required Credential Manager libraries. +2. [**Instantiate Credential Manager**](https://developer.android.com/identity/passkeys/create-passkeys#instantiate): Create a Credential Manager instance. +3. [**Get credential creation options from the app server**](https://developer.android.com/identity/passkeys/create-passkeys#get-options): From your app server, send the client app the details required to create the passkey, such as information about the app, the user, as well as a `challenge` and other fields. +4. [**Request a passkey**](https://developer.android.com/identity/passkeys/create-passkeys#request-passkey): In your app, use the details received from the app server to create a [`GetPublicKeyCredentialOption`](https://developer.android.com/reference/androidx/credentials/GetPublicKeyCredentialOption) object and use this object to invoke the `credentialManager.getCredential()` method to create a passkey. +5. [**Handle the passkey creation response**](https://developer.android.com/identity/passkeys/create-passkeys#handle-response): When you receive the credentials on your client app, you must encode, serialize and then send the public key to the app server. You must also handle each of the exceptions that can occur in case of passkey creation. +6. [**Verify and save the public key on the server**](https://developer.android.com/identity/passkeys/create-passkeys#verify): Complete the server-side steps to verify the origin of the credential and then save the public key. +7. [**Notify the user**](https://developer.android.com/identity/passkeys/create-passkeys#notify): Notify the user that their passkey has been created. + +> [!TIP] +> **Tip:** To enhance the user experience during sign up, add functionality to [restore credentials](https://developer.android.com/identity/sign-in/restore-credentials) on a new device to let users seamlessly set up their existing accounts on new Android devices, and [suppressing autofill dialogs on +> credential fields](https://developer.android.com/identity/passkeys/create-passkeys#suppress-autofill). + +## Add dependencies to your app + +Add the following dependencies to your app module's `build.gradle` file: + +### Kotlin + +```kotlin +dependencies { + implementation("androidx.credentials:credentials:1.7.0-alpha03") + implementation("androidx.credentials:credentials-play-services-auth:1.7.0-alpha03") +} +``` + +### Groovy + +```groovy +dependencies { + implementation "androidx.credentials:credentials:1.7.0-alpha03" + implementation "androidx.credentials:credentials-play-services-auth:1.7.0-alpha03" +} +``` + +> [!NOTE] +> **Note:** Use the latest available versions of the dependencies. Versions of [`androidx.credentials`](https://developer.android.com/identity/passkeys/create-passkeys#automatic-upgrade) earlier than version 1.2 throw an `UnsupportedOperationException("Post-U not supported yet")` exception on Android 14. + +## Instantiate Credential Manager + +Use your app or activity context to create a `CredentialManager` object. + + // Use your app or activity context to instantiate a client instance of + // CredentialManager. + private val credentialManager = CredentialManager.create(context) + +## Get credential creation options from your app server + +When the user clicks a "Create Passkey" button or when a new user signs up, make +a request from your app to your app server to obtain the information required to +start the passkey registration process. + +Use a FIDO-compliant library in your app server to send your client app the +information required to create a passkey, such as information about the user, +the app, and additional configuration properties. To learn more, see [Server +side passkey registration](https://developers.google.com/identity/passkeys/developer-guides/server-registration). + +In the client app, decode the public key creation options sent by the app +server. These are usually represented in JSON format. To learn more about how +this decoding is done for web clients, see [Encoding and +Decoding](https://developers.google.com/identity/passkeys/developer-guides/server-registration#encoding_and_decoding). For Android client apps, you must handle the decoding +separately. + +> [!NOTE] +> **Note:** On your app server, securely store the `challenge` so that you can later verify the origin of the credential. + +The following snippet shows the structure the public key creation options sent +by the app server: + + { + "challenge": "", + "rp": { + "name": "", + "id": "" + }, + "user": { + "id": "", + "name": "", + "displayName": "" + }, + "pubKeyCredParams": [ + { + "type": "public-key", + "alg": -7 + } + ], + "attestation": "none", + "excludeCredentials": [ + { + "id": "", + "type": "public-key" + } + ], + "authenticatorSelection": { + "requireResidentKey": true, + "residentKey": "required", + "userVerification": "required" + } + } + +Key fields in the public key creation options include: + +- `challenge`: A server-generated random string that is used to prevent replay attacks. +- `rp`: Details about the app. + - `rp.name`: The app's name. + - `rp.id`: The app's domain or subdomain. +- `user`: Details about the user. + - `id`: The user's unique ID. This value must not include personally identifying information, for example, email addresses or usernames. You can use a random, 16-byte value. + - `name`: A unique identifier for the account that the user will recognise, such as their email address or username. This will be displayed in the account selector. If using a username, use the same value as in password authentication. + - `displayName`: An optional, user-friendly name for the account intended for display in the account selector. +- `authenticatorSelection`: Details about the device that will be used for authentication. + - `authenticatorAttachment`: Indicates the preferred [authenticator](https://www.w3.org/TR/webauthn/#authenticator). The possible values are as follows: + - `platform`: This value is used for an authenticator built into the user's device, such as a fingerprint sensor. + - `cross-platform`: This value is used for roaming devices such as security keys. It is not typically used in the passkey context. + - Unspecified (recommended): Leaving this value unspecified provides users with the flexibility to create passkeys on their preferred devices. In most cases, leaving the parameter unspecified is the best option. + - `requireResidentKey`: To create a passkey, set the value of this `Boolean` field to `true`. + - `residentKey`: To create a passkey, set the value to `required`. + - `userVerification`: Used to specify the requirements for user verification during a passkey registration. The possible values are as follows: + - `preferred`: Use this value if you prioritize user experience over protection, such as in environments where user verification causes more friction than protection. + - `required`: Use this value if invoking a user verification method available on the device is required. + - `discouraged`: Use this value if using a user verification method is discouraged. + To learn more about `userVerification`, see [userVerification deep dive](https://web.dev/articles/webauthn-user-verification). +- `excludeCredentials`: List credential IDs in an [array](https://w3c.github.io/webauthn/#dom-publickeycredentialcreationoptions-excludecredentials) to prevent the creation of a duplicate passkey if one already exists with the same credential provider. + +## Create a passkey + +After you have parsed the server-side public key creation options, create a +passkey by wrapping these options in a `CreatePublicKeyCredentialRequest` object +and calling `createCredential()`. + +The `createPublicKeyCredentialRequest` includes the following: + +- `requestJson`: The credential creation options sent by the app server. +- `preferImmediatelyAvailableCredentials`: This is an optional Boolean field that defines whether to only use locally-available or credential provider-synced credentials to fulfill the request, instead of credentials from security keys or [hybrid](https://w3c.github.io/webauthn/#dom-authenticatortransport-hybrid) key flows. The possible usages are as follows: + - `false` (default): Use this value if the call to Credential Manager was triggered by an explicit user action. + - `true`: Use this value if Credential Manager is opportunistically called, such as when first opening the app. + If you set the value to `true` and there are no immediately available credentials, Credential Manager won't show any UI and the request will fail immediately, returning NoCredentialException for get requests and [`CreateCredentialNoCreateOptionException`](https://developer.android.com/reference/kotlin/androidx/credentials/exceptions/CreateCredentialNoCreateOptionException) for create requests. +- `origin`: This field is automatically set for Android apps. For browsers and similarly privileged apps that need to set `origin`, see [Make Credential + Manager calls on behalf of other parties for privileged apps](https://developer.android.com/training/sign-in/privileged-apps). +- `isConditional`: This is an optional field that defaults to `false`. For more information, see [Automatically create a passkey](https://developer.android.com/identity/passkeys/create-passkeys#automatic-upgrade). + +Calling the `createCredential()` function launches Credential Manager's built-in +bottom sheet UI that prompts the user to use a passkey and to select a +credential provider and account for storage. However, if `isConditional` is set +to `true`, the bottom sheet UI does not display, and the passkey is +automatically created. + +### Automatically create a passkey + +You can automatically create a passkey for a user after a successful password +login by setting the `isConditional` parameter to `true` in your +`CreatePublicKeyCredentialRequest` while creating a passkey. If the user doesn't +already have a passkey, your app will automatically attempt to create one in the +background and store it in the user's credential provider, such as Google +Password Manager. For an example of how this is implemented, see the [public +sample](https://github.com/android/identity-samples/blob/main/Shrine/app/src/main/java/com/authentication/shrine/ui/AuthenticationScreen.kt#L98). +![An example of the notification Google Password Manager shows after passkey creation](https://developer.android.com/static/identity/passkeys/images/conditional-create-gpm.svg) **Figure 2:**Google Password Manager notification + +> [!NOTE] +> **Note:** If a passkey is created automatically, credential providers are responsible for notifying users about a newly created passkey. Google Password Manager notifies users when a passkey is automatically created. However, other credential providers might have their own conditions and notifications for this feature. + +## Handle the response + +After the user is verified using the device's screen lock, a passkey is created +and stored in the user's selected credential provider. + +The response after you successfully call `createCredential()` is a +[PublicKeyCredential](https://developer.android.com/jetpack/androidx/releases/credentials) object. + +The `PublicKeyCredential` looks as follows: + + { + "id": "", + "type": "public-key", + "rawId": "", + "response": { + "clientDataJSON": "", + "attestationObject": "" + }, + "authenticatorAttachment": "platform" + } + +In the client app, serialize the object and send it to the app server. + +Add code to handle failures as shown in the following snippet: + + fun handleFailure(e: CreateCredentialException) { + when (e) { + is CreatePublicKeyCredentialDomException -> { + // Handle the passkey DOM errors thrown according to the + // WebAuthn spec. + } + is CreateCredentialCancellationException -> { + // The user intentionally canceled the operation and chose not + // to register the credential. + } + is CreateCredentialInterruptedException -> { + // Retry-able error. Consider retrying the call. + } + is CreateCredentialProviderConfigurationException -> { + // Your app is missing the provider configuration dependency. + // Most likely, you're missing the + // "credentials-play-services-auth" module. + } + is CreateCredentialCustomException -> { + // You have encountered an error from a 3rd-party SDK. If you + // make the API call with a request object that's a subclass of + // CreateCustomCredentialRequest using a 3rd-party SDK, then you + // should check for any custom exception type constants within + // that SDK to match with e.type. Otherwise, drop or log the + // exception. + } + else -> Log.w(TAG, "Unexpected exception type ${e::class.java.name}") + } + } + +## Verify and save the public key on the app server + +On the app server, you must verify the public key credential and then [save the +public key](https://web.dev/articles/passkey-registration#save-credential). + +To verify the public key credential's origin, compare it against an allow list +of approved apps. If a key has an unrecognized origin, reject it. + +> [!NOTE] +> **Note:** An app's origin is based on its unique identity, which is the SHA-256 fingerprint of its signing certificate. + +To obtain the app's SHA 256 fingerprint: + +1. Print your release app's signing certificate by running the following + command in a terminal: + + keytool -list -keystore + + In the response, identify the signing certificate's SHA 256 fingerprint, + mentioned as `Certificate fingerprints block` : `SHA256`. +2. Encode the SHA256 fingerprint with base64url encoding. This Python example + demonstrates how to properly encode the fingerprint: + + import binascii + import base64 + fingerprint = '' # your app's SHA256 fingerprint + print(base64.urlsafe_b64encode(binascii.a2b_hex(fingerprint.replace(':', ''))).decode('utf8').replace('=', '')) + +3. Append `android:apk-key-hash`: to the start of the output from the previous + step so that you get something that is similar to the following: + + android:apk-key-hash: + + The result should match with an allowed origin on your app server. If you + have multiple signing certificates, such as certificates for debugging and + release, or multiple apps, then repeat the process and accept all the + origins as valid on the app server. + +> [!NOTE] +> **Note:** When you save the passkey on the app server, make sure that you save the Authenticator Attestation Globally Unique Identifier ([AAGUID](https://web.dev/articles/webauthn-aaguid)) from the client data. The AAGUID is a unique number that identifies the model of the authenticator. For more information, see [Manage passkeys](https://developer.android.com/identity/passkeys/manage-passkeys). + +## Notify the user + +After the passkey is successfully created, notify your users about the passkey +and inform them that they can manage their passkeys from their credential +provider app or from [within the app settings](https://developer.android.com/identity/passkeys/manage-passkeys). Notify users by using a +custom dialog, notification, or snackbar. Since an unexpected passkey creation +by a malicious entity requires an immediate security alert, consider +supplementing these in-app methods with external communication, such as an +email. + +## Enhance the user experience + +To enhance the user experience while implementing sign up with Credential +Manager, consider adding functionality for restore credentials and suppress +autofill dialogs. + +### Add functionality to restore credentials on a new device + +To allow users to seamlessly log into their accounts on a new device, implement +the [Restore Credentials](https://developer.android.com/identity/sign-in/restore-credentials) functionality. Adding restore credentials with +`BackupAgent` logs users in when they open your restored app on a new device, +letting them use your app right away. + +### Suppress autofill on credential fields (optional) + +For app screens where users are expected to use Credential Manager's bottom +sheet UI for authentication, add the `isCredential` attribute to the username +and password fields. This suppresses autofill dialogs (`FillDialog` and +`SaveDialog`) from overlapping with Credential Manager's bottom sheet UI. + +The `isCredential` attribute is supported on Android 14 and higher. + +The following example demonstrates how you can add the `isCredential` attribute +to the relevant username and password fields in the relevant views for your app: + + + +## Next steps + +- [Sign in with passkeys](https://developer.android.com/identity/passkeys/sign-in-with-passkeys) +- [Manage passkeys](https://developer.android.com/identity/passkeys/manage-passkeys) +- [Understand passkey user experience flows](https://developer.android.com/design/ui/mobile/guides/patterns/passkeys) \ No newline at end of file diff --git a/identity/restore-credentials/references/android/identity/passkeys/sign-in-with-passkeys.md b/identity/restore-credentials/references/android/identity/passkeys/sign-in-with-passkeys.md new file mode 100644 index 0000000..3f36518 --- /dev/null +++ b/identity/restore-credentials/references/android/identity/passkeys/sign-in-with-passkeys.md @@ -0,0 +1,216 @@ +This guide continues on the implementation of using passkeys for authentication. +Before your users can sign in with passkeys, you must also complete the +instructions in [Create passkeys](https://developer.android.com/identity/passkeys/create-passkeys). + +To authenticate with a passkey, you must first retrieve the options required to +retrieve the public key from your [app server](https://developer.android.com/identity/credential-manager#authentication-terminology), and then call the Credential +Manager API to retrieve the public key. Then, handle the sign-in response +appropriately. + +> [!TIP] +> **Tip:** While designing authentication flows with passkeys, make sure you follow the recommendations in the [UX guidelines for passkeys](https://developer.android.com/design/ui/mobile/guides/patterns/passkeys). + +## Overview + +This guide focuses on the changes required in your client app to sign in your +user with a passkey, and gives a brief overview of the app server-side +implementation. To learn more about server-side integration, see [Server-side +passkey authentication](https://developers.google.com/identity/passkeys/developer-guides/server-authentication). + +To retrieve all the passkey and password options that are associated with the +user's account, complete these steps: + +1. [**Get credential request options from the server**](https://developer.android.com/identity/passkeys/sign-in-with-passkeys#get-options): Make a request from your app to your authentication server to start the passkey sign-in process. From the server, send the options required to get the public key credential, as well as a unique challenge. +2. [**Create the object required to get the public key credential**](https://developer.android.com/identity/passkeys/sign-in-with-passkeys#create-object): Wrap the options sent by the server in a `GetPublicKeyCredentialOption` object +3. ([**optional) Prepare getCredential**](https://developer.android.com/identity/passkeys/sign-in-with-passkeys#reduce-latency): In Android 14 and higher, you can reduce latency by showing the account selector by using the `prepareGetCredential()` method before calling `getCredential()`. +4. [**Launch the sign in flow**](https://developer.android.com/identity/passkeys/sign-in-with-passkeys#launch-sign-in): Call `getCredential()` method to sign in the user +5. [**Handle the response**](https://developer.android.com/identity/passkeys/sign-in-with-passkeys#handle-response): Handle each of the possible credential responses. +6. [**Handle exceptions**](https://developer.android.com/identity/passkeys/sign-in-with-passkeys#handle-exceptions): Make sure that you handle exceptions appropriately. + +> [!TIP] +> **Tip:** Enhance the user experience during sign in by also adding functionality to [restore credentials](https://developer.android.com/identity/sign-in/restore-credentials) on a new device to let users seamlessly set up their existing accounts on new Android devices. + +## Get credential request options from the server + +Request the server for the options required to get the public key credentials, +as well as the `challenge`, which is unique for each sign-in attempt. To learn +more about the server-side implementation, see [Create the +challenge](https://developers.google.com/identity/passkeys/developer-guides/server-authentication#create_the_challenge) and [Create credential request +options](https://developers.google.com/identity/passkeys/developer-guides/server-authentication#create_credential_request_options). + +The options look similar to the following: + + { + "challenge": "", + "allowCredentials": [], + "rpId": "" + } + +To learn more about the fields, see the blogpost about [signing in with a +passkey](https://web.dev/articles/passkey-form-autofill#fetch_information_from_the_backend). + +> [!TIP] +> **Tip:** To reduce wait times in the UI because of as network calls, fetch the details required from the app server at the beginning of the user's app session. + +## Create the object required to get the public key credential + +In your app, use the options to create a `GetPublicKeyCredentialOption` object. +In the following example, `requestJson` represents the options sent by the +server. + + // Get password logins from the credential provider on the user's device. + val getPasswordOption = GetPasswordOption() + + // Get passkeys from the credential provider on the user's device. + val getPublicKeyCredentialOption = GetPublicKeyCredentialOption( + requestJson = requestJson + ) + +Then, wrap `GetPublicKeyCredentialOption` in a `GetCredentialRequest` object. + + val credentialRequest = GetCredentialRequest( + // Include all the sign-in options that your app supports. + listOf(getPasswordOption, getPublicKeyCredentialOption), + // Defines whether you prefer to use only immediately available + // credentials or hybrid credentials. + preferImmediatelyAvailableCredentials = preferImmediatelyAvailableCredentials + ) + +> [!NOTE] +> **Note:** [`GetCredentialRequest`](https://developer.android.com/reference/androidx/credentials/GetCredentialRequest#GetCredentialRequest) can include an `origin` field. However, `origin` is automatically set for Android apps, and hence should be left as null. For browsers and similarly privileged apps that need to set `origin`, see [Make Credential Manager calls on behalf of other parties for privileged +> apps](https://developer.android.com/training/sign-in/privileged-apps). + +## Optional: Reduce sign-in latency + +On Android 14 or higher, you can reduce latency when showing the account +selector by using the [`prepareGetCredential()`](https://developer.android.com/reference/androidx/credentials/CredentialManager#prepareGetCredential) method before calling +`getCredential()`. + +The `prepareGetCredential()` method returns a +[`PrepareGetCredentialResponse`](https://developer.android.com/reference/androidx/credentials/PrepareGetCredentialResponse) object which is cached. This lets the +`getCredential()` method in the following step bring up the account selector +with the cached data. + + coroutineScope { + val response = credentialManager.prepareGetCredential( + GetCredentialRequest( + listOf( + // Include all the sign-in options that your app supports + getPublicKeyCredentialOption, + getPasswordOption + ) + ) + ) + } + +## Launch the sign-in flow + +Call the `getCredential()` method to show the user the account selector. Use the +following code snippet as a reference for how to launch the sign-in flow: + + // Use an activity-based context to avoid undefined system UI + // launching behavior. + val context = MutableContextWrapper(activityContext) + coroutineScope { + try { + result = credentialManager.getCredential( + // Use MutableContextWrapper to avoid memory leak during configuration changes + context = context, + request = credentialRequest + ) + handleSignIn(result) + } catch (e: GetCredentialException) { + // Handle failure + } + } + +> [!NOTE] +> **Note:** If you call `getCredential()` but the user does not have any credentials available, or if they don't grant consent to using their credentials, a `NoCredentialException` is returned. To learn more about this exception, see the [troubleshooting guide](https://developer.android.com/identity/sign-in/credential-manager-troubleshooting-guide). + +## Handle the response + +Handle the response, which can contain one of various types of credential +objects. + + fun handleSignIn(result: GetCredentialResponse) { + // Handle the successfully returned credential. + val credential = result.credential + + when (credential) { + is PublicKeyCredential -> { + val responseJson = credential.authenticationResponseJson + // Share responseJson i.e. a GetCredentialResponse on your server to + // validate and authenticate + } + + is PasswordCredential -> { + val username = credential.id + val password = credential.password + // Use id and password to send to your server to validate + // and authenticate + } + + is CustomCredential -> { + // If you are also using any external sign-in libraries, parse them + // here with the utility functions provided. + if (credential.type == ExampleCustomCredential.TYPE) { + try { + val ExampleCustomCredential = + ExampleCustomCredential.createFrom(credential.data) + // Extract the required credentials and complete the authentication as per + // the federated sign in or any external sign in library flow + } catch (e: ExampleCustomCredential.ExampleCustomCredentialParsingException) { + // Unlikely to happen. If it does, you likely need to update the dependency + // version of your external sign-in library. + Log.e(TAG, "Failed to parse an ExampleCustomCredential", e) + } + } else { + // Catch any unrecognized custom credential type here. + Log.e(TAG, "Unexpected type of credential") + } + } + else -> { + // Catch any unrecognized credential type here. + Log.e(TAG, "Unexpected type of credential") + } + } + } + +The `PublicKeyCredential` returned from authentication is essentially a signed +assertion, structured as follows: + + { + "id": "", + "type": "public-key", + "rawId": "", + "response": { + "clientDataJSON": "", + "authenticatorData": "", + "signature": "", + "userHandle": "" + } + } + +On the server, you must verify the credential. To learn more, see [Verify and +sign in the user](https://developers.google.com/identity/passkeys/developer-guides/server-authentication#verify_and_sign_in_the_user). + +## Handle exceptions + +You should handle all the subclass exceptions of [`GetCredentialException`](https://developer.android.com/reference/androidx/credentials/exceptions/GetCredentialException). +To learn how to handle each exception, see the [troubleshooting guide](https://developer.android.com/identity/sign-in/credential-manager-troubleshooting-guide). + + coroutineScope { + try { + result = credentialManager.getCredential( + context = activityContext, + request = credentialRequest + ) + } catch (e: GetCredentialException) { + Log.e("CredentialManager", "No credential available", e) + } + } + +## Next steps + +- [Manage passkeys](https://developer.android.com/identity/passkeys/manage-passkeys) +- [Understand passkey user experience flows](https://developer.android.com/design/ui/mobile/guides/patterns/passkeys) \ No newline at end of file diff --git a/identity/restore-credentials/skill.md b/identity/restore-credentials/skill.md new file mode 100644 index 0000000..1651e90 --- /dev/null +++ b/identity/restore-credentials/skill.md @@ -0,0 +1,319 @@ +## Fundamentals + +The objective is to implement the **Restore Credentials** feature through the +Android Credential Manager API (`androidx.credentials`). This allows apps that +use or are integrating Credential Manager to silently log users back in when +they restore their app on a new device. Restore Credentials operates +independently of the app's primary authentication method (passwords, passkeys, +federated sign-in) and requires no UI changes to existing sign-in flows. + +### Scope + +**Crucial:** This skill focuses exclusively on the Android client-side +integration. It does **not** implement the server-side cryptographic +validation logic. The developer must be reminded of this and the +[Points to inform the developer about](https://developer.android.com/agents/skills/identity/restore-credentials/skill#backend-guidelines) after implementation is done. + +## Implementation Guidelines + +When instructed to implement Restore Credentials on a developer's application, +remember the following: + +1. Before the implementation, you **MUST** read and understand the [Two-Tier + Restoration Architecture](https://developer.android.com/agents/skills/identity/restore-credentials/skill#two-tier-restoration-architecture) and review the [DOs and DON'Ts](https://developer.android.com/agents/skills/identity/restore-credentials/skill#dos-and-donts). +2. After the implementation, you **MUST** present the developer with the [Backend Guidelines](https://developer.android.com/agents/skills/identity/restore-credentials/skill#backend-guidelines) as a reminder for their backend setup. It is important that you remind the developer that they still have to implement the backend. + +## Two-Tier Restoration Architecture + +To enable a resilient sign-in experience, retrieve credentials through a +**two-tier architecture**: + +1. **Tier 1 (Primary - Background):** Executes automatically during device setup using the app's `BackupAgent.onRestoreFinished()` callback. This provides an invisible restoration before the user opens the app for the first time, allowing background sync and notification delivery. +2. **Tier 2 (Secondary - Foreground):** Runs in the Launcher `Activity.onCreate()` to catch failovers if background restoration didn't complete (example: dropped network, delayed restoration) or if `allowBackup` is disabled. + +If `allowBackup` in the manifest is set to true, implement both. Otherwise, only +implement tier 2 (Foreground Restoration). Do **NOT** change the value of +`allowBackup` in the manifest. + +## DOs and DON'Ts + +**DO:** + +- Do check `AndroidManifest.xml` for the value of allowBackup to determine what you have to implement. +- Do implement a fallback for createCredential: always try calling it first with `isCloudBackupEnabled` set to true. If an `E2eeUnavailableException` is thrown, catch it and retry the call with `isCloudBackupEnabled` set to `false`. +- Do implement a `BackupAgent` (subclass of `android.app.backup.BackupAgent`) if `allowBackup` is true in the manifest. +- Call `clearCredentialState()` when the user signs out. This is a mandatory security measure to log the user out fully. +- Do attempt to get the restore key on the first launch of the app on a new device and also within the `BackupAgent.onRestoreFinished()` callback if your app uses it. +- Do ensure that a restore credential is created even if the user is already logged in. +- Do ensure that the credential retrieval and login in `onRestoreFinished()` is performed synchronously (for example using `runBlocking`). +- Do restore notifications in the `BackupAgent` if your app uses them. (For example capture and send FCM token to backend) +- Do ensure that if you implement mock network requests or stubs, you replace all placeholders with valid, properly formatted JSON payloads for the credential requests. +- Do encapsulate credential creation and retrieval into their own dedicated functions. Because credential creation must be called in multiple places (sign-up, sign-in) and retrieval across multiple tiers (`BackupAgent` and Launcher `Activity`), this prevents code duplication. +- Do remind the developer of the [critical guidelines](https://developer.android.com/agents/skills/identity/restore-credentials/skill#implementation-guide) for implementing the backend once you're done with the implementation. +- Do generate a separate restore key for each application if the organization has multiple apps with different package names, as a restore key is tied to a unique application package name. + +**DON'T:** + +- DON'T change the value of `allowBackup` in `AndroidManifest.xml`. Restore Credentials functionality is not affected by the allowBackup setting, meaning the user will still be automatically logged in when a Restore Credential exists, even if `allowBackup` is false. +- DON'T implement a `BackupAgent` if `allowBackup` is `false` in `AndroidManifest.xml`. +- DON'T assume the credential stored in the `GetCredentialResponse` to be of type `PublicKeyCredential`. It has type `RestoreCredential`. +- DON'T chain `GetRestoreCredentialOption` with any other `CredentialOption` in the construction of a `GetCredentialRequest`. +- DON'T assume `CredentialManager` or the Android system will automatically delete a restore key when a user signs out of the app. You must explicitly call `clearCredentialState` with a `ClearCredentialStateRequest` of `TYPE_CLEAR_RESTORE_CREDENTIAL`. +- DON'T remove any existing calls to `clearCredentialState()`. A `ClearCredentialStateRequest` without a type specified only clears all NON-restore credentials. + +## Implementation Guide + +Implement the Android client-side code by using the following guide. Follow it +**step-by-step** and don't implement any backend functionality, only remind +the user of the [Backend Guidelines](https://developer.android.com/agents/skills/identity/restore-credentials/skill#backend-guidelines) once you're done. + +## Version compatibility + +Credential Manager's Restore Credentials works on devices running Android 9 and +higher, Google Play services (GMS) core version 24220000 or higher, and version +1.5.0 or higher of the `androidx.credentials` library. + +## Prerequisites + +Set up a [relying party server](https://developer.android.com/agents/skills/identity/restore-credentials/skill#backend-guidelines) similar to the server for [passkeys](https://developer.android.com/agents/skills/identity/restore-credentials/skill#dos-and-donts). If +you already have a [server](https://developer.android.com/agents/skills/identity/restore-credentials/skill#two-tier-restoration-architecture) set up to handle authentication with passkeys, +use the same server-side implementation for restore keys. + +> [!NOTE] +> **Note:** While the server-side implementation is the same for passkeys and restore keys, your client-side app can support restore keys without supporting passkeys. Because restore keys work independently of the authentication method in your app (for example, passwords or Sign in with Google), you don't need to make any additional changes to the existing authentication methods in your app's code. + +## Dependencies + +Add the following dependencies to your app module's `build.gradle` file: + +### Kotlin + +```kotlin +dependencies { + implementation("androidx.credentials:credentials:1.7.0-alpha03") + implementation("androidx.credentials:credentials-play-services-auth:1.7.0-alpha03") +} +``` + +### Groovy + +```groovy +dependencies { + implementation "androidx.credentials:credentials:1.7.0-alpha03" + implementation "androidx.credentials:credentials-play-services-auth:1.7.0-alpha03" +} +``` + +Restore Credentials is available from version 1.5.0 and higher of the +androidx.credentials library. However, it's recommended to use the latest stable +versions of the dependencies where possible. + +> [!NOTE] +> **Note:** The Restore Credentials feature works regardless of whether [`allowBackup`](https://developer.android.com/guide/topics/manifest/application-element#allowbackup) is set in the `manifest`. + +## Overview + +1. [**Create a restore key**](https://developer.android.com/agents/skills/identity/restore-credentials/skill#create-restore-key): To create a restore key, complete the following steps: + 1. [**Instantiate Credential Manager**](https://developer.android.com/agents/skills/identity/restore-credentials/skill#implementation-guide): Create a `CredentialManager` object. + 2. [**Get credential creation options from the app server**](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API): Send the client app the details required to create the restore key from your app server. + 3. [**Create the restore key**](https://w3c.github.io/webauthn/#dictdef-publickeycredentialcreationoptionsjson): Create a restore key for the user's account if the user is signed in to your app. + 4. [**Handle the credential creation response**](https://w3c.github.io/webauthn/#dictdef-publickeycredentialrequestoptionsjson): Send the credentials from your client app to your app server for processing, and handle any exceptions. +2. [**Sign in with a restore key**](https://developer.android.com/agents/skills/identity/restore-credentials/skill#sign-restore): To sign in with a restore key, complete the following steps: + 1. [**Get credential retrieval options from the app server**](https://developer.android.com/agents/skills/identity/restore-credentials/skill#get-credential-retrieval): Send the client app the details required to retrieve the restore key from your app server. + 2. [**Get the restore key**](https://developer.android.com/agents/skills/identity/restore-credentials/skill#get-restore): Request the restore key from Credential Manager when the user sets up a new device. This lets the user sign in without additional input. + 3. [**Handle the credential retrieval response**](https://developer.android.com/agents/skills/identity/restore-credentials/skill#handle-sign-in): Send the restore key from the client app to the app server to sign in the user. +3. [**Delete a restore key**](https://developer.android.com/agents/skills/identity/restore-credentials/skill#delete-restore). + +## Create a restore key + +Your app should cover all cases of a user signing in to ensure active users have +a restore key created. Create the restore key in the following scenarios: + +- If the user is signed in and a restore key isn't already created (such as in the `onCreate` method for the main `Activity`). +- When the user is signing in or completing a new account registration flow. + +To optimize performance and avoid the overhead of creating or checking for a +restore credential on every single login, set a `boolean` flag or a credential +creation timestamp in local storage, such as `has_synced_restore_credential`, to +track whether the key has already been created. + +> [!NOTE] +> **Note:** A restore key is tied to an application's unique package name. If your organization's main app and sub-apps have different package names, create a separate restore key for each app. + +### Instantiate Credential Manager + +Use your app's activity context to instantiate a `CredentialManager` object. + + // Use your app or activity context to instantiate a client instance of + // CredentialManager. + private val credentialManager = CredentialManager.create(context) + +### Get credential creation options from your app server + +Use a FIDO-compliant library in your app server to send your client app the +information required to create the restore credential, such as information about +the user, the app, and additional configuration properties. For more information +about the server-side implementation, see [Server-side +guidance](https://developers.google.com/identity/passkeys/developer-guides/server-registration). + +### Create the restore key + +After parsing the public key creation options sent by the server, create a +restore key by wrapping these options in a +[`CreateRestoreCredentialRequest`](https://developer.android.com/reference/androidx/credentials/CreateRestoreCredentialRequest) object and calling the +[`createCredential()`](https://developer.android.com/reference/androidx/credentials/CredentialManager#createCredential(android.content.Context,androidx.credentials.CreateCredentialRequest)) method with the `CredentialManager` object. + + // createRestoreRequest contains the details sent by the server + val response = credentialManager.createCredential(context, createRestoreRequest) + +#### Key points about the code + +- The `CreateRestoreCredentialRequest` object contains the following fields: + + - `requestJson`: The credential creation options sent by the app server in the [Web Authentication API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API) format for [`PublicKeyCredentialCreationOptionsJSON`](https://w3c.github.io/webauthn/#dictdef-publickeycredentialcreationoptionsjson). + - `isCloudBackupEnabled`: `Boolean` field to determine if the restore key + should be backed up to the cloud. By default, this flag is `true`. This + field has these values: + + - `true`: (**Recommended**) This value enables the backup of restore keys to the cloud if the user has Google Backup and end-to-end encryption, such as a screen lock, enabled. + - `false`: This value saves the key locally and not in the cloud. The key is not available on the new device if the user chooses to restore from the cloud. + + > [!CAUTION] + > **Caution:** It is recommended to set `isCloudBackupEnabled` to `true`. If cloud backup is disabled and the user restores from a cloud backup, the call to retrieve the restore key fails. Users who restore your app with a cloud backup don't receive the restore key and are not automatically signed in. + +### Handle the credential creation response + +The Credential Manager API returns a response of type +[`CreateRestoreCredentialResponse`](https://developer.android.com/reference/androidx/credentials/CreateRestoreCredentialResponse). This response holds the public key +credential registration response in [JSON format](https://w3c.github.io/webauthn/#authenticatorattestationresponse). + +Send the public key from your app to the relying party server. This public key +is similar to the public key generated when you create a passkey. The same code +that handles passkey creation on the server can also handle restore key +creation. For more information about the server-side implementation, see [the +guidance for passkeys](https://developer.android.com/identity/passkeys/create-passkeys). + +During the restore key creation process, handle these exceptions: + +- [`CreateRestoreCredentialDomException`](https://developer.android.com/reference/androidx/credentials/exceptions/restorecredential/CreateRestoreCredentialDomException): This exception occurs if `requestJson` is invalid and does not follow the WebAuthn format for [`PublicKeyCredentialCreationOptionsJSON`](https://w3c.github.io/webauthn/#dictdef-publickeycredentialcreationoptionsjson). +- [`E2eeUnavailableException`](https://developer.android.com/reference/androidx/credentials/exceptions/restorecredential/E2eeUnavailableException): This exception occurs if `isCloudBackupEnabled` is `true`, but the user's device does not have data backup or end-to-end encryption, such as a screen lock. + To ensure that Restore Credentials are created in all cases, you must handle the `E2eeUnavailableException` explicitly by calling `createCredential` with `isCloudBackupEnabled` set to `true`. If `E2eeUnavailableException` is thrown, catch and call `createCredential` again with `isCloudBackupEnabled` set to `false`. +- `IllegalArgumentException`: This exception occurs if `createRestoreRequest` is empty or not valid JSON, or if it does not have a valid `user.id` that conforms to the WebAuthn [specifications](https://w3c.github.io/webauthn/#dictdef-publickeycredentialcreationoptionsjson). + +## Sign in with a restore key + +Use Restore Credentials to silently sign in the user during the device setup +process. + +### Get credential retrieval options from the app server + +Send the client app the options required to get the restore key from the server. +For similar passkey guidance for this step, see [Sign in with a passkey](https://developer.android.com/identity/passkeys/sign-in-with-passkeys#get-options). +For more information about the server-side implementation, see the [server-side +authentication guide](https://developers.google.com/identity/passkeys/developer-guides/server-authentication#create_credential_request_options). + +### Get the restore key + +To get the restore key on the new device, call the `getCredential()` method on +the `CredentialManager` object. + +It is recommended to fetch the restore key in both of the following scenarios: + +- On the first launch of the app on the device. Credential restoration in this scenario is independent of restoration of the app data. +- If app data backup and restore is enabled, get the restore key immediately after the app data is restored. Use [`BackupAgent`](https://developer.android.com/reference/android/app/backup/BackupAgent) to configure your app's backup and ensure you complete the `getCredential` functionality within the [`onRestoreFinished`](https://developer.android.com/reference/android/app/backup/BackupAgent#onRestoreFinished()) callback. Don't use the `onRestore` method, as it is only called for key-value backups, whereas `onRestoreFinished` is reliably called for any kind of backup restore. This avoids potential delays when users open their new device for the first time and lets users interact with the app without waiting for them to open your app. For example, this lets your app send the user notifications before they open the app for the first time on the new device, which is particularly relevant for messaging or communications apps. + +> [!IMPORTANT] +> **Important:** Notifications aren't automatically restored after the restore credentials are retrieved. If you use Firebase to handle notifications, you must fetch and send the Firebase Cloud Messaging (FCM) token to the backend to successfully resume background messaging and notifications. + + // Fetch the options required to get the restore key + val authenticationJson = fetchAuthenticationJson() + + // Create the GetRestoreCredentialRequest object + val options = GetRestoreCredentialOption(authenticationJson) + val getRequest = GetCredentialRequest(listOf(options)) + + val response = credentialManager.getCredential(context, getRequest) + + // Type-check and extract the restore credential + val credential = response.credential as RestoreCredential + +The credential manager APIs return a response of type +[`GetCredentialResponse`](https://developer.android.com/reference/android/credentials/GetCredentialResponse). The credential contained in this response is +explicitly of type `RestoreCredential`, which holds the public key. + +### Handle the sign-in response + +Send the public key from the app to the relying party server, which can then be +used to sign in the user. On the server side, this action is similar to signing +in using a passkey. The same code that handles sign-in with passkeys on the +server can also handle sign-ins with restore keys. For more information about +the server-side implementation for passkeys, see [Sign in with a passkey](https://developer.android.com/identity/passkeys/sign-in-with-passkeys). + +> [!NOTE] +> **Note:** Even though restore keys and passkeys use the same underlying server implementation, differentiate between them when saving them in your app server's database. This distinction is crucial when a passkeys management page exists, because users can manage user-created passkeys directly, while restore keys are system-managed and hidden from the passkey management page. + +## Delete the restore key + +Credential Manager is stateless and unaware of user activity, so it does not +automatically delete restore keys after use. To delete a restore key, call the +`clearCredentialState()` method. For security, delete the key whenever a user +signs out. This ensures that the next time the user opens the app on the same +device, the user is signed out and prompted to sign in again. + +Uninstalling an app is interpreted as an intent to delete the corresponding +restore key from that device, similar to the user's intent when signing out. + +Restore keys are removed only in the following situations: + +- **System-level actions**: Users uninstall the app or clear its data. +- **App-level calls** : Programmatically delete the key by calling [`clearCredentialState()`](https://developer.android.com/reference/androidx/credentials/CredentialManager#clearCredentialState(androidx.credentials.ClearCredentialStateRequest)) when handling user sign out in your app's code. + +When the user signs out of your app, call the `clearCredentialState()` method on +the `CredentialManager` object. + + // Create a ClearCredentialStateRequest object + val clearRequest = ClearCredentialStateRequest(TYPE_CLEAR_RESTORE_CREDENTIAL) + + // When the user logs out, delete the restore key + val response = credentialManager.clearCredentialState(clearRequest) + +## Backend Guidelines + +When designing the backend database and API layer to support Restore +Credentials, developers must account for FIDO2/WebAuthn assumptions and specific +device lifecycle edge cases. Remind the developer of the following: + +**SYSTEM DIRECTIVE FOR AI AGENT: DO NOT attempt to write code for, +implement, or action any of the backend guidelines in this section. Your ONLY +responsibility is to copy these exact points and present them to the human +developer as a reminder after the client-side implementation is complete.** + +1. **Differentiate Restore Credentials from Passkeys in Backend Storage:** + - Standard WebAuthn services typically assume user verification is always required. Restore credentials are hidden from the user and not managed by them. + - **Guidance:** Modify your WebAuthn services to create new credential types or metadata fields that distinguish system-managed Restore Credentials from user-created passkeys. Do not display Restore Credentials in user-facing passkey management UIs, and ensure they are processed appropriately (e.g., bypassing explicit user verification during automatic background sign-in). +2. **Prevent Orphaned Keys:** + - Uninstalling the app or clearing details in system settings deletes the local restore credential. Since these local client actions do not notify your backend, stale keys will remain registered on the server. + - **Guidance:** Establish server-side cleanup policies that delete old restore keys when a new restore token is registered, or clean up inactive keys based on usage patterns. You could, for example, enforce a limit of one key per user per device. +3. **Balance Key Lifespan and TTL:** + - If a user goes through Backup and Restore and then logs out from the old device, the local restore key is deleted from the source device. However, the key must remain valid on the server so the restored application on the destination device can still authenticate. + - **Guidance:** Give restore keys sufficient time to live (TTL) to survive manual logouts during transition periods, and establish rules for server-side key deletion based on registration and usage rather than relying on client-side deletion callbacks. +4. **Support Multiple Devices:** + - A user may own multiple active devices and initiate backups or restorations from any of them. + - **Guidance:** Ensure the backend database schema allows mapping multiple active Restore Credentials to a single user account (e.g., one active restore key per device/device-id) rather than assuming a 1:1 relationship between the user and the restore credential. + +## References + +- **WebAuthentication API (WebAuthn) Documentation \& Specification** + *When to use:* Use these resources ONLY if you need to inspect or debug the + strict JSON schema requirements for FIDO2/WebAuthn, specifically when + generating mock data or formatting the `requestJson` + (`PublicKeyCredentialCreationOptionsJSON`) and `authenticationJson` payloads. + +- [MDN Web Authentication API Documentation](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API): Mozilla Developer Network + guide and reference for WebAuthn APIs + +- [W3C `PublicKeyCredentialCreationOptionsJSON`](https://w3c.github.io/webauthn/#dictdef-publickeycredentialcreationoptionsjson): Data structure definition + for WebAuthn credential creation requests in JSON format. + +- [W3C `PublicKeyCredentialRequestOptionsJSON`](https://w3c.github.io/webauthn/#dictdef-publickeycredentialrequestoptionsjson): Data structure definition + for WebAuthn authentication or assertion requests in JSON format. \ No newline at end of file diff --git a/identity/verified-email/SKILL.md b/identity/verified-email/SKILL.md index 0b8b191..c16a661 100644 --- a/identity/verified-email/SKILL.md +++ b/identity/verified-email/SKILL.md @@ -8,7 +8,7 @@ description: Provides a complete workflow for implementing verified email retrie license: Complete terms in LICENSE.txt metadata: author: Google LLC - last-updated: '2026-08-06' + last-updated: '2026-08-19' keywords: - implementation - Android @@ -57,11 +57,15 @@ Email verification is applicable for the following use cases: ### Scope \& Pre-requisites -**Crucial** : This skill focuses exclusively on the **Android client-side integration** . It does **not** implement the app's server-side cryptographic validation logic. Server-side validation of the returned credential is required for security and must be implemented in your backend. +**Crucial** : This skill focuses exclusively on the **Android client-side +integration** . It does **not** implement the app's server-side cryptographic +validation logic. Server-side validation of the returned credential is required +for security and must be implemented in your backend. ## Codebase exploration for Use Cases -Get started with the following queries in project source code to find relevant screens with different use cases to implement verified email: +Get started with the following queries in project source code to find relevant +screens with different use cases to implement verified email: - `SignUpScreen` - `"Email address"` @@ -72,11 +76,14 @@ Get started with the following queries in project source code to find relevant s ## Identifying Integration Points -To implement this feature effectively, you must first locate the relevant flows in your codebase. To initiate, start with the following strategies to cater to different use cases using verified email: +To implement this feature effectively, you must first locate the relevant +flows in your codebase. To initiate, start with the following strategies to +cater to different use cases using verified email: ### 1. Search for Navigation Routes -If your app uses Navigation, search for routes or destinations related to authentication: +If your app uses Navigation, search for routes or destinations related +to authentication: Look for: @@ -85,14 +92,16 @@ Look for: ### 2. Locate Authentication ViewModels -Find the business logic handling user attributes and account creation, account recovery: +Find the business logic handling user attributes and account creation, account +recovery: - **Keywords** : `SignUpViewModel`, `AuthViewModel`, `RegistrationRepository`. - **Code Pattern** : Look for methods like `onCreateAccount`, `onRecoverAccount`, or `validateEmail`. ### 3. Find instances of reauthentication for sensitive actions -For reauthentication use cases, find areas where users perform sensitive actions: +For reauthentication use cases, find areas where users perform sensitive +actions: - **Keywords** : `ChangePassword`, `UpdatePayment`, `DeleteAccount`, `UpdateDetails`, `EditUserDetails` @@ -110,11 +119,14 @@ For reauthentication use cases, find areas where users perform sensitive actions ## Overview -This guide describes how to implement verified email retrieval using the [Digital Credentials Verifier API](references/android/identity/digital-credentials/credential-verifier.md) through an [OpenID for Verifiable Presentations (OpenID4VP)](https://openid.net/specs/openid-4-verifiable-presentations-1_0.html) request. +This guide describes how to implement verified email retrieval using the +[Digital Credentials Verifier API](references/android/identity/digital-credentials/credential-verifier.md) through an [OpenID for Verifiable +Presentations (OpenID4VP)](https://openid.net/specs/openid-4-verifiable-presentations-1_0.html) request. ## Add dependencies -In your app's `build.gradle` file, add the following dependencies for Credential Manager: +In your app's `build.gradle` file, add the following dependencies for Credential +Manager: ### Kotlin @@ -144,9 +156,14 @@ Use your app or activity context to create a `CredentialManager` object. ## Construct the Digital Credential request -To request a verified email, construct a [`GetCredentialRequest`](https://developer.android.com/reference/android/credentials/GetCredentialRequest) containing a [`GetDigitalCredentialOption`](https://developer.android.com/reference/androidx/credentials/GetDigitalCredentialOption). This option requires a `requestJson` string formatted as an OpenID for Verifiable Presentations (OpenID4VP) request. +To request a verified email, construct a [`GetCredentialRequest`](https://developer.android.com/reference/android/credentials/GetCredentialRequest) +containing a [`GetDigitalCredentialOption`](https://developer.android.com/reference/androidx/credentials/GetDigitalCredentialOption). This option requires a +`requestJson` string formatted as an OpenID for Verifiable Presentations +(OpenID4VP) request. -The OpenID4VP request JSON must follow a specific structure. The current providers support a JSON structure with an outer `"digital": {"requests": [...]}` wrapper. +The OpenID4VP request JSON must follow a specific structure. The current +providers support a JSON structure with an outer `"digital": {"requests": +[...]}` wrapper. val nonce = generateSecureRandomNonce() @@ -191,19 +208,26 @@ The OpenID4VP request JSON must follow a specific structure. The current provide The request contains the following key information: -- **DCQL query** : The `dcql_query` specifies the credential type and the claims being requested (`email_verified`). You can request other claims to determine the level of verification. A few possible claims are as follows: +- **DCQL query** : The `dcql_query` specifies the credential type and the + claims being requested (`email_verified`). You can request other claims to + determine the level of verification. A few possible claims are as follows: - `email_verified`: In the response, this is a Boolean that indicates whether the email is verified. - `hd` (hosted domain): In the response, this is empty. > [!NOTE] - > **Note:** If `email_verified` is `true` and `hd` is empty in the response, it implies that the account is an authorized Google Account. Google does not issue [verifiable credentials](references/android/identity/digital-credentials/index.md) for Google Workspace Accounts. However, the `hd` field is present in verifiable credentials issued for non-workspace accounts. You are encouraged to implement handling this field to future-proof your app. If the email is non-@gmail.com, Google verified this email when the Google Account was created, but there is no freshness claim. Therefore, for non-Google emails, you should consider an additional challenge, such as an OTP, to verify the user. To understand the schema of the credential and the specific rules for validating fields like `email_verified`, refer to the [Google Identity guides](https://developers.google.com/identity/gsi/web/guides/verify-google-id-token). + > **Note:** If `email_verified` is `true` and `hd` is empty in the response, it implies that the account is an authorized Google Account. Google does not issue [verifiable credentials](references/android/identity/digital-credentials/index.md) for Google Workspace Accounts. However, the `hd` field is present in verifiable credentials issued for non-workspace accounts. You are encouraged to implement handling this field to future-proof your app. If the email is non-@gmail.com, Google verified this email when the Google Account was created, but there is no freshness claim. Therefore, for non-Google emails, you should consider an additional challenge, such as an OTP, to verify the user. To understand the schema of the credential and the specific rules for validating fields like `email_verified`, refer to the [Google + > Identity guides](https://developers.google.com/identity/gsi/web/guides/verify-google-id-token). -- **nonce**: A unique, cryptographically secure random value is generated for each request. This is critical for security, as it prevents replay attacks. +- **nonce**: A unique, cryptographically secure random value is generated for + each request. This is critical for security, as it prevents replay attacks. -- `UserInfoCredential`: This value implies a specific type of digital credential that contains user attributes. Including this in the request is pivotal to distinguish the email verification use case. +- `UserInfoCredential`: This value implies a specific type of digital + credential that contains user attributes. Including this in the request is + pivotal to distinguish the email verification use case. -Next, wrap the `openId4vpRequest` JSON in a `GetDigitalCredentialOption`, create a `GetCredentialRequest`, and call `getCredential()`. +Next, wrap the `openId4vpRequest` JSON in a `GetDigitalCredentialOption`, create +a `GetCredentialRequest`, and call `getCredential()`. > [!NOTE] > **Note:** The `hd` and `email_verified` fields are hidden from users in Credential Manager's built-in UI. You cannot make a request with only these hidden fields- in case of such requests, the response is the [`GetCredentialCancellationException`](https://developer.android.com/reference/kotlin/androidx/credentials/exceptions/GetCredentialCancellationException). @@ -239,12 +263,15 @@ Present the user with the request, using the Credential Manager built-in UI. ## Parse the response on the client -After receiving the response, you can perform a preliminary parse on the client. This is useful for immediately updating the UI, for example, by showing the user's name. +After receiving the response, you can perform a preliminary parse on the client. +This is useful for immediately updating the UI, for example, by showing the +user's name. > [!IMPORTANT] > **Important:** This step is not for validation. Full cryptographic verification must be performed on your server. -The following code extracts the raw [Selective Disclosure JWT (SD-JWT)](https://datatracker.ietf.org/doc/rfc9901/) and uses a helper to decode its claims. +The following code extracts the raw [Selective Disclosure JWT +(SD-JWT)](https://datatracker.ietf.org/doc/rfc9901/) and uses a helper to decode its claims. // 1. Parse the outer JSON wrapper to get the `vp_token` val responseData = JSONObject(responseJsonString) @@ -269,19 +296,25 @@ The following code extracts the raw [Selective Disclosure JWT (SD-JWT)](https:// ## Handle the response -The Credential Manager API will return a [`DigitalCredential`](https://developer.android.com/reference/androidx/credentials/DigitalCredential) response. +The Credential Manager API will return a [`DigitalCredential`](https://developer.android.com/reference/androidx/credentials/DigitalCredential) +response. -The following is an example of what the raw `responseJsonString` looks like, and what the claims look like after parsing the inner SD-JWT where you get additional metadata as well along with verified email: +The following is an example of what the raw `responseJsonString` looks like, and +what the claims look like after parsing the inner SD-JWT where you get +additional metadata as well along with verified email: /* // Example of the raw JSON response from credential.credentialJson: { - "vp_token": { - // This key matches the 'id' you set in your dcql_query - "user_info_query": [ - // The SD-JWT string (Issuer JWT ~ Disclosures ~ Key Binding JWT) - "eyJhbGciOiJ...~WyI...IiwgImVtYWlsIiwgInVzZXJAZXhhbXBsZS5jb20iXQ~...~eyJhbGciOiJ..." - ] + "protocol": "openid4vp-v1-unsigned", + "data": { + "vp_token": { + // This key matches the 'id' you set in your dcql_query + "user_info_query": [ + // The SD-JWT string (Issuer JWT ~ Disclosures ~ Key Binding JWT) + "eyJhbGciOiJ...~WyI...IiwgImVtYWlsIiwgInVzZXJAZXhhbXBsZS5jb20iXQ~...~eyJhbGciOiJ..." + ] + } } } @@ -309,11 +342,18 @@ The following is an example of what the raw `responseJsonString` looks like, and ## Server-side validation for account creation -Since the retrieved email is cryptographically verified, you can omit the email OTP verification step, significantly reducing sign-up friction and potentially increasing conversion. This process is best handled on your server. The client sends the raw response (containing the `vp_token`) and the original nonce to a new server endpoint. +Since the retrieved email is cryptographically verified, you can omit the email +OTP verification step, significantly reducing sign-up friction and potentially +increasing conversion. This process is best handled on your server. The client +sends the raw response (containing the `vp_token`) and the original nonce to a +new server endpoint. -For verification, your application must send the full `responseJsonString` to your server for cryptographic validation before creating an account or logging the user in. +For verification, your application must send the full `responseJsonString` to +your server for cryptographic validation before creating an account or logging +the user in. -The digital credential provides two critical levels of verification for your server: +The digital credential provides two critical levels of verification for your +server: - **Authenticity of the data** : Verifying the issuer (`iss`) URL and the `SD-JWT` signature proves that a trusted authority issued this data. - **Identity of the presenter** : Verifying the `cnf` field and the Key Binding (`kb`) signature confirms that the credential is being shared by the same device it was originally issued to, preventing it from being intercepted or used on another device. @@ -324,11 +364,15 @@ The validation on the server must achieve the following: - **Verify signature**: Check the signature of the SD-JWT using the public keys (JWKs) available at https://verifiablecredentials-pa.googleapis.com/.well-known/vc-public-jwks. > [!NOTE] -> **Note:** Use a standard library (such as [@sd-jwt/sd-jwt-vc](https://datatracker.ietf.org/doc/rfc9901/) for Node.js) to perform the verification steps as outlined in the [OpenID for Verifiable Presentations specification](https://openid.net/specs/openid-4-verifiable-presentations-1_0.html). +> **Note:** Use a standard library (such as [@sd-jwt/sd-jwt-vc](https://datatracker.ietf.org/doc/rfc9901/) for Node.js) to perform the verification steps as outlined in the [OpenID for Verifiable +> Presentations specification](https://openid.net/specs/openid-4-verifiable-presentations-1_0.html). -For full security, make sure that you also validate the `nonce` to prevent replay attacks. +For full security, make sure that you also validate the `nonce` to prevent +replay attacks. -By combining these steps, your server can validate both the authenticity of the data and the identity of the presenter, ensuring the credential wasn't intercepted or spoofed before provisioning the new account. +By combining these steps, your server can validate both the authenticity of the +data and the identity of the presenter, ensuring the credential wasn't +intercepted or spoofed before provisioning the new account. try { // Send the raw credential response and the original nonce to your server. @@ -351,11 +395,17 @@ By combining these steps, your server can validate both the authenticity of the ## Passkey creation -An optional but highly recommended next step after provisioning an account is to immediately [create a passkey](references/android/identity/passkeys/create-passkeys.md) for that account. This provides a secure, passwordless method for the user to sign in. This flow is identical to a standard passkey registration. +An optional but highly recommended next step after provisioning an account is to +immediately [create a passkey](references/android/identity/passkeys/create-passkeys.md) for that account. This provides a secure, +passwordless method for the user to sign in. This flow is identical to a +standard passkey registration. ## WebView support -For the flow to work on a [`WebView`](https://developer.android.com/reference/android/webkit/WebView), developers should implement a [JavaScript bridge](references/android/identity/sign-in/credential-manager-webview.md) (JS Bridge) to facilitate the handoff. This bridge allows the `WebView` object to signal the native app, which can then perform the actual call to the Credential Manager API. +For the flow to work on a [`WebView`](https://developer.android.com/reference/android/webkit/WebView), developers should implement a +[JavaScript bridge](references/android/identity/sign-in/credential-manager-webview.md) (JS Bridge) to facilitate the handoff. This bridge +allows the `WebView` object to signal the native app, which can then perform the +actual call to the Credential Manager API. ## See also @@ -364,7 +414,8 @@ For the flow to work on a [`WebView`](https://developer.android.com/reference/an ## Critical Security Guidelines -To maintain the integrity of the email verification flow, the following security requirements are mandatory: +To maintain the integrity of the email verification flow, the following security +requirements are mandatory: - **Server-side Validation** : Never trust claims parsed on the client for security-sensitive operations like account creation. Send the complete, raw `responseJsonString` and the original `nonce` to the app's server for full verification. - **Nonce Integrity** : Generate a unique, cryptographically secure nonce for every request and **never** reuse a nonce across multiple requests to prevent replay attacks. diff --git a/identity/verified-email/references/android/identity/credential-manager/index.md b/identity/verified-email/references/android/identity/credential-manager/index.md index d11d29b..c646f19 100644 --- a/identity/verified-email/references/android/identity/credential-manager/index.md +++ b/identity/verified-email/references/android/identity/credential-manager/index.md @@ -1,15 +1,21 @@ -[Credential Manager](https://developer.android.com/reference/kotlin/androidx/credentials/package-summary) is the recommended Jetpack API for credential exchange in Android apps. The Credential Manager API streamlines credential exchange across form factors for use cases spanning authentication and authorization. You can also use Credential Manager to handle digital credentials and restore user credentials on new Android devices. +[Credential Manager](https://developer.android.com/reference/kotlin/androidx/credentials/package-summary) is the recommended Jetpack API for credential exchange +in Android apps. The Credential Manager API streamlines credential exchange +across form factors for use cases spanning authentication and authorization. You +can also use Credential Manager to handle digital credentials and restore user +credentials on new Android devices. ## Credential Manager features -Credential Manager streamlines the sign-in process and enhances security by offering a robust set of capabilities, including: +Credential Manager streamlines the sign-in process and enhances security by +offering a robust set of capabilities, including: - **Support for various authentication mechanisms** : Enables users to sign up or sign in to your app using: - [Passkeys](https://developer.android.com/identity/passkeys) - [Sign-in with Google](https://developer.android.com/identity/sign-in/credential-manager-siwg) and other federated sign-in mechanisms - [Passwords](https://developer.android.com/identity/passwords) - [Digital credentials](https://developer.android.com/identity/digital-credentials) -- **Cross-device credential restore** : Provides integration with [Restore Credentials](https://developer.android.com/identity/sign-in/restore-credentials), allowing users to seamlessly start using your app on a new device. +- **Cross-device credential restore** : Provides integration with [Restore + Credentials](https://developer.android.com/identity/sign-in/restore-credentials), allowing users to seamlessly start using your app on a new device. - **Seamless integration with credential providers** : Offers support for [credential providers](https://developer.android.com/identity/sign-in/credential-provider), including password managers such as Google Password Manager. - **Credential management**: Enables updating the metadata for user credentials, helping to keep credentials consistent across your app and credential providers. - **WebView compatibility** : Works with apps that use [WebView](https://developer.android.com/identity/sign-in/credential-manager-webview). @@ -17,10 +23,12 @@ Credential Manager streamlines the sign-in process and enhances security by offe ## Benefits of using Credential Manager -Adopting Credential Manager provides several key advantages for both your application and your users: +Adopting Credential Manager provides several key advantages for both your +application and your users: - **Enhanced security**: Passkeys offer enhanced security and protect users from phishing attempts. -- **Support for various form factors** : The APIs work across Android [form factors](https://developer.android.com/identity/form-factors), including mobile, Android XR, and Wear OS devices. +- **Support for various form factors** : The APIs work across Android [form + factors](https://developer.android.com/identity/form-factors), including mobile, Android XR, and Wear OS devices. - **Simplified authentication across use cases** : [Digital credentials](https://developer.android.com/identity/digital-credentials), such as digital driver's licenses, corporate IDs, and national ID cards, enable authentication across various use cases, including those requiring [phone number verification](https://developer.android.com/identity/digital-credentials/phone-number-verification). - **Improved user experience**: Credential Manager's unified interface gives users a familiar and consistent experience and improves registration and sign-in speeds. The bottom sheet UI appears inline to your app's content, so your users remain within your app's context during sign-in. The following image shows Credential Manager's built-in UI: @@ -28,7 +36,8 @@ Adopting Credential Manager provides several key advantages for both your applic ## Authentication terminology -An entity that requires authentication for its users is known as a **relying party**. An authentication workflow typically includes the following components: +An entity that requires authentication for its users is known as a **relying +party**. An authentication workflow typically includes the following components: - **Relying party client app**: The client---in this case, your Android app---that handles the user interface to create and use passkeys. - **Relying party server**: An app server that helps with the creation, storage, and verification of passkeys. @@ -36,13 +45,21 @@ An entity that requires authentication for its users is known as a **relying par ## Credential storage -Credential providers, such as Google Password Manager, provide a centralized and secure location for users to manage credentials across their devices, further simplifying the authentication process. To integrate with Credential Manager as a credential provider, see [Implement authentication as a credential provider](https://developer.android.com/identity/sign-in/credential-provider). +Credential providers, such as Google Password Manager, provide a centralized and +secure location for users to manage credentials across their devices, further +simplifying the authentication process. To integrate with Credential Manager as +a credential provider, see +[Implement authentication as a credential provider](https://developer.android.com/identity/sign-in/credential-provider). -For more information about how Google Password Manager keeps your credentials secure, see [Security of Passkeys in the Google Password Manager](https://security.googleblog.com/2022/10/SecurityofPasskeysintheGooglePasswordManager.html). +For more information about how Google Password Manager keeps your credentials +secure, see +[Security of Passkeys in the Google Password Manager](https://security.googleblog.com/2022/10/SecurityofPasskeysintheGooglePasswordManager.html). ## Migrate to Credential Manager -Credential Manager is intended to replace legacy Android authentication APIs and local FIDO2 credentials. For more information about migrating to Credential Manager, see the following guides: +Credential Manager is intended to replace legacy Android authentication APIs and +local FIDO2 credentials. For more information about migrating to Credential +Manager, see the following guides: - [Google Sign-In](https://developer.android.com/identity/sign-in/legacy-gsi-migration) - [One Tap](https://developer.android.com/identity/legacy/one-tap) diff --git a/identity/verified-email/references/android/identity/digital-credentials/credential-verifier.md b/identity/verified-email/references/android/identity/digital-credentials/credential-verifier.md index 082ada7..c66bf50 100644 --- a/identity/verified-email/references/android/identity/digital-credentials/credential-verifier.md +++ b/identity/verified-email/references/android/identity/digital-credentials/credential-verifier.md @@ -1,10 +1,18 @@ -Digital credential verification within Android apps can be used to authenticate and authorize a user's identity (such as a government ID), properties about that user (such as a driver's license, academic degree, or attributes such as age or address), or other scenarios where a credential needs to be issued and verified to assert the authenticity of an entity. +Digital credential verification within Android apps can be used to authenticate +and authorize a user's identity (such as a government ID), properties about that +user (such as a driver's license, academic degree, or attributes such as age or +address), or other scenarios where a credential needs to be issued and verified +to assert the authenticity of an entity. -Digital Credentials is a public W3C standard that specifies how to access a user's verifiable digital credentials from a digital wallet, and is implemented for web use cases with the [W3C Credential Management API](https://www.w3.org/TR/credential-management-1/). On Android, Credential Manager's [`DigitalCredential`](https://developer.android.com/reference/kotlin/androidx/credentials/DigitalCredential) API is used for verifying digital credentials. +Digital Credentials is a public W3C standard that specifies how to access a +user's verifiable digital credentials from a digital wallet, and is implemented +for web use cases with the [W3C Credential Management API](https://www.w3.org/TR/credential-management-1/). On +Android, Credential Manager's [`DigitalCredential`](https://developer.android.com/reference/kotlin/androidx/credentials/DigitalCredential) API is used for +verifying digital credentials. ### Android version compatibility -The Verifier API is supported on Android 9 (API level 28) and higher. +The Verifier API is supported on Android 6 (API level 23) and higher. ### Implementation @@ -29,7 +37,8 @@ Next, Initialize an instance of the `CredentialManager` class. #### Construct a digital credential request -Construct a digital credential request and use it to initialize a `DigitalCredentialOption`. +Construct a digital credential request and use it to initialize a +`DigitalCredentialOption`. // The request in the JSON format to conform with // the JSON-ified Credential Manager - Verifier API request definition. @@ -42,7 +51,8 @@ Construct a digital credential request and use it to initialize a `DigitalCreden listOf(digitalCredentialOption) ) -Here is an example of an OpenId4Vp request. A full reference can be found at this [website](https://openid.net/specs/openid-4-verifiable-presentations-1_0.html). +Here is an example of an OpenId4Vp request. A full reference can be found at +this [website](https://openid.net/specs/openid-4-verifiable-presentations-1_0.html). { "requests": [ @@ -90,9 +100,14 @@ Here is an example of an OpenId4Vp request. A full reference can be found at thi #### Get the credential -Launch the `getCredential` flow with the constructed request. You will receive either a successful `GetCredentialResponse`, or a `GetCredentialException` if the request fails. +Launch the `getCredential` flow with the constructed request. You will receive +either a successful `GetCredentialResponse`, or a `GetCredentialException` if +the request fails. -The `getCredential` flow triggers Android system dialogs to present the user's available credential options and collect their selection. Next, the wallet app that contains the chosen credential option will display UIs to collect consent and perform actions needed to generate a digital credential response. +The `getCredential` flow triggers Android system dialogs to present the user's +available credential options and collect their selection. Next, the wallet app +that contains the chosen credential option will display UIs to collect consent +and perform actions needed to generate a digital credential response. coroutineScope.launch { try { diff --git a/identity/verified-email/references/android/identity/digital-credentials/email-verification-implementation.md b/identity/verified-email/references/android/identity/digital-credentials/email-verification-implementation.md index 048f11b..63e45b0 100644 --- a/identity/verified-email/references/android/identity/digital-credentials/email-verification-implementation.md +++ b/identity/verified-email/references/android/identity/digital-credentials/email-verification-implementation.md @@ -1,12 +1,10 @@ -
- ## Android skills [View on GitHub](https://github.com/android/skills/tree/main/identity/verified-email) ### Retrieve verified email -Use an Android skill to integrate a secure, OTP-less email verification flow into your app. To install the skill from the [Android CLI](https://developer.android.com/tools/agents/android-cli), run: +Use an [Android skill](https://developer.android.com/tools/agents/android-skills) to integrate a secure, OTP-less email verification flow into your app. To install the skill from the [Android CLI](https://developer.android.com/tools/agents/android-cli), run: android skills add --skill verified-email @@ -14,11 +12,14 @@ Use an Android skill to integrate a secure, OTP-less email verification flow int ## Overview -This guide describes how to implement verified email retrieval using the [Digital Credentials Verifier API](https://developer.android.com/identity/digital-credentials/credential-verifier) through an [OpenID for Verifiable Presentations (OpenID4VP)](https://openid.net/specs/openid-4-verifiable-presentations-1_0.html) request. +This guide describes how to implement verified email retrieval using the +[Digital Credentials Verifier API](https://developer.android.com/identity/digital-credentials/credential-verifier) through an [OpenID for Verifiable +Presentations (OpenID4VP)](https://openid.net/specs/openid-4-verifiable-presentations-1_0.html) request. ## Add dependencies -In your app's `build.gradle` file, add the following dependencies for Credential Manager: +In your app's `build.gradle` file, add the following dependencies for Credential +Manager: ### Kotlin @@ -48,9 +49,14 @@ Use your app or activity context to create a `CredentialManager` object. ## Construct the Digital Credential request -To request a verified email, construct a [`GetCredentialRequest`](https://developer.android.com/reference/android/credentials/GetCredentialRequest) containing a [`GetDigitalCredentialOption`](https://developer.android.com/reference/androidx/credentials/GetDigitalCredentialOption). This option requires a `requestJson` string formatted as an OpenID for Verifiable Presentations (OpenID4VP) request. +To request a verified email, construct a [`GetCredentialRequest`](https://developer.android.com/reference/android/credentials/GetCredentialRequest) +containing a [`GetDigitalCredentialOption`](https://developer.android.com/reference/androidx/credentials/GetDigitalCredentialOption). This option requires a +`requestJson` string formatted as an OpenID for Verifiable Presentations +(OpenID4VP) request. -The OpenID4VP request JSON must follow a specific structure. The current providers support a JSON structure with an outer `"digital": {"requests": [...]}` wrapper. +The OpenID4VP request JSON must follow a specific structure. The current +providers support a JSON structure with an outer `"digital": {"requests": +[...]}` wrapper. val nonce = generateSecureRandomNonce() @@ -95,19 +101,26 @@ The OpenID4VP request JSON must follow a specific structure. The current provide The request contains the following key information: -- **DCQL query** : The `dcql_query` specifies the credential type and the claims being requested (`email_verified`). You can request other claims to determine the level of verification. A few possible claims are as follows: +- **DCQL query** : The `dcql_query` specifies the credential type and the + claims being requested (`email_verified`). You can request other claims to + determine the level of verification. A few possible claims are as follows: - `email_verified`: In the response, this is a Boolean that indicates whether the email is verified. - `hd` (hosted domain): In the response, this is empty. > [!NOTE] - > **Note:** If `email_verified` is `true` and `hd` is empty in the response, it implies that the account is an authorized Google Account. Google does not issue [verifiable credentials](https://developer.android.com/identity/digital-credentials#verifiable-credentials) for Google Workspace Accounts. However, the `hd` field is present in verifiable credentials issued for non-workspace accounts. You are encouraged to implement handling this field to future-proof your app. If the email is non-@gmail.com, Google verified this email when the Google Account was created, but there is no freshness claim. Therefore, for non-Google emails, you should consider an additional challenge, such as an OTP, to verify the user. To understand the schema of the credential and the specific rules for validating fields like `email_verified`, refer to the [Google Identity guides](https://developers.google.com/identity/gsi/web/guides/verify-google-id-token). + > **Note:** If `email_verified` is `true` and `hd` is empty in the response, it implies that the account is an authorized Google Account. Google does not issue [verifiable credentials](https://developer.android.com/identity/digital-credentials#verifiable-credentials) for Google Workspace Accounts. However, the `hd` field is present in verifiable credentials issued for non-workspace accounts. You are encouraged to implement handling this field to future-proof your app. If the email is non-@gmail.com, Google verified this email when the Google Account was created, but there is no freshness claim. Therefore, for non-Google emails, you should consider an additional challenge, such as an OTP, to verify the user. To understand the schema of the credential and the specific rules for validating fields like `email_verified`, refer to the [Google + > Identity guides](https://developers.google.com/identity/gsi/web/guides/verify-google-id-token). -- **nonce**: A unique, cryptographically secure random value is generated for each request. This is critical for security, as it prevents replay attacks. +- **nonce**: A unique, cryptographically secure random value is generated for + each request. This is critical for security, as it prevents replay attacks. -- `UserInfoCredential`: This value implies a specific type of digital credential that contains user attributes. Including this in the request is pivotal to distinguish the email verification use case. +- `UserInfoCredential`: This value implies a specific type of digital + credential that contains user attributes. Including this in the request is + pivotal to distinguish the email verification use case. -Next, wrap the `openId4vpRequest` JSON in a `GetDigitalCredentialOption`, create a `GetCredentialRequest`, and call `getCredential()`. +Next, wrap the `openId4vpRequest` JSON in a `GetDigitalCredentialOption`, create +a `GetCredentialRequest`, and call `getCredential()`. > [!NOTE] > **Note:** The `hd` and `email_verified` fields are hidden from users in Credential Manager's built-in UI. You cannot make a request with only these hidden fields- in case of such requests, the response is the [`GetCredentialCancellationException`](https://developer.android.com/reference/kotlin/androidx/credentials/exceptions/GetCredentialCancellationException). @@ -143,12 +156,15 @@ Present the user with the request, using the Credential Manager built-in UI. ## Parse the response on the client -After receiving the response, you can perform a preliminary parse on the client. This is useful for immediately updating the UI, for example, by showing the user's name. +After receiving the response, you can perform a preliminary parse on the client. +This is useful for immediately updating the UI, for example, by showing the +user's name. > [!IMPORTANT] > **Important:** This step is not for validation. Full cryptographic verification must be performed on your server. -The following code extracts the raw [Selective Disclosure JWT (SD-JWT)](https://datatracker.ietf.org/doc/rfc9901/) and uses a helper to decode its claims. +The following code extracts the raw [Selective Disclosure JWT +(SD-JWT)](https://datatracker.ietf.org/doc/rfc9901/) and uses a helper to decode its claims. // 1. Parse the outer JSON wrapper to get the `vp_token` val responseData = JSONObject(responseJsonString) @@ -173,19 +189,25 @@ The following code extracts the raw [Selective Disclosure JWT (SD-JWT)](https:// ## Handle the response -The Credential Manager API will return a [`DigitalCredential`](https://developer.android.com/reference/androidx/credentials/DigitalCredential) response. +The Credential Manager API will return a [`DigitalCredential`](https://developer.android.com/reference/androidx/credentials/DigitalCredential) +response. -The following is an example of what the raw `responseJsonString` looks like, and what the claims look like after parsing the inner SD-JWT where you get additional metadata as well along with verified email: +The following is an example of what the raw `responseJsonString` looks like, and +what the claims look like after parsing the inner SD-JWT where you get +additional metadata as well along with verified email: /* // Example of the raw JSON response from credential.credentialJson: { - "vp_token": { - // This key matches the 'id' you set in your dcql_query - "user_info_query": [ - // The SD-JWT string (Issuer JWT ~ Disclosures ~ Key Binding JWT) - "eyJhbGciOiJ...~WyI...IiwgImVtYWlsIiwgInVzZXJAZXhhbXBsZS5jb20iXQ~...~eyJhbGciOiJ..." - ] + "protocol": "openid4vp-v1-unsigned", + "data": { + "vp_token": { + // This key matches the 'id' you set in your dcql_query + "user_info_query": [ + // The SD-JWT string (Issuer JWT ~ Disclosures ~ Key Binding JWT) + "eyJhbGciOiJ...~WyI...IiwgImVtYWlsIiwgInVzZXJAZXhhbXBsZS5jb20iXQ~...~eyJhbGciOiJ..." + ] + } } } @@ -213,11 +235,18 @@ The following is an example of what the raw `responseJsonString` looks like, and ## Server-side validation for account creation -Since the retrieved email is cryptographically verified, you can omit the email OTP verification step, significantly reducing sign-up friction and potentially increasing conversion. This process is best handled on your server. The client sends the raw response (containing the `vp_token`) and the original nonce to a new server endpoint. +Since the retrieved email is cryptographically verified, you can omit the email +OTP verification step, significantly reducing sign-up friction and potentially +increasing conversion. This process is best handled on your server. The client +sends the raw response (containing the `vp_token`) and the original nonce to a +new server endpoint. -For verification, your application must send the full `responseJsonString` to your server for cryptographic validation before creating an account or logging the user in. +For verification, your application must send the full `responseJsonString` to +your server for cryptographic validation before creating an account or logging +the user in. -The digital credential provides two critical levels of verification for your server: +The digital credential provides two critical levels of verification for your +server: - **Authenticity of the data** : Verifying the issuer (`iss`) URL and the `SD-JWT` signature proves that a trusted authority issued this data. - **Identity of the presenter** : Verifying the `cnf` field and the Key Binding (`kb`) signature confirms that the credential is being shared by the same device it was originally issued to, preventing it from being intercepted or used on another device. @@ -228,11 +257,15 @@ The validation on the server must achieve the following: - **Verify signature**: Check the signature of the SD-JWT using the public keys (JWKs) available at https://verifiablecredentials-pa.googleapis.com/.well-known/vc-public-jwks. > [!NOTE] -> **Note:** Use a standard library (such as [@sd-jwt/sd-jwt-vc](https://datatracker.ietf.org/doc/rfc9901/) for Node.js) to perform the verification steps as outlined in the [OpenID for Verifiable Presentations specification](https://openid.net/specs/openid-4-verifiable-presentations-1_0.html). +> **Note:** Use a standard library (such as [@sd-jwt/sd-jwt-vc](https://datatracker.ietf.org/doc/rfc9901/) for Node.js) to perform the verification steps as outlined in the [OpenID for Verifiable +> Presentations specification](https://openid.net/specs/openid-4-verifiable-presentations-1_0.html). -For full security, make sure that you also validate the `nonce` to prevent replay attacks. +For full security, make sure that you also validate the `nonce` to prevent +replay attacks. -By combining these steps, your server can validate both the authenticity of the data and the identity of the presenter, ensuring the credential wasn't intercepted or spoofed before provisioning the new account. +By combining these steps, your server can validate both the authenticity of the +data and the identity of the presenter, ensuring the credential wasn't +intercepted or spoofed before provisioning the new account. try { // Send the raw credential response and the original nonce to your server. @@ -255,11 +288,17 @@ By combining these steps, your server can validate both the authenticity of the ## Passkey creation -An optional but highly recommended next step after provisioning an account is to immediately [create a passkey](https://developer.android.com/identity/passkeys/create-passkeys) for that account. This provides a secure, passwordless method for the user to sign in. This flow is identical to a standard passkey registration. +An optional but highly recommended next step after provisioning an account is to +immediately [create a passkey](https://developer.android.com/identity/passkeys/create-passkeys) for that account. This provides a secure, +passwordless method for the user to sign in. This flow is identical to a +standard passkey registration. ## WebView support -For the flow to work on a [`WebView`](https://developer.android.com/reference/android/webkit/WebView), developers should implement a [JavaScript bridge](https://developer.android.com/identity/sign-in/credential-manager-webview) (JS Bridge) to facilitate the handoff. This bridge allows the `WebView` object to signal the native app, which can then perform the actual call to the Credential Manager API. +For the flow to work on a [`WebView`](https://developer.android.com/reference/android/webkit/WebView), developers should implement a +[JavaScript bridge](https://developer.android.com/identity/sign-in/credential-manager-webview) (JS Bridge) to facilitate the handoff. This bridge +allows the `WebView` object to signal the native app, which can then perform the +actual call to the Credential Manager API. ## See also diff --git a/identity/verified-email/references/android/identity/digital-credentials/email-verification.md b/identity/verified-email/references/android/identity/digital-credentials/email-verification.md index 66a5f1a..2134577 100644 --- a/identity/verified-email/references/android/identity/digital-credentials/email-verification.md +++ b/identity/verified-email/references/android/identity/digital-credentials/email-verification.md @@ -1,4 +1,7 @@ -This document describes using Credential Manager to get a cryptographically verified email address from a user's device. This process removes the need for your app users to verify their email with one-time passwords (OTPs) or magic links. +This document describes using Credential Manager to get a cryptographically +verified email address from a user's device. This process removes the need for +your app users to verify their email with one-time passwords (OTPs) or magic +links. This document explains the following areas: @@ -16,66 +19,90 @@ This guide assumes you are familiar with the following concepts: ## Android compatibility -This feature is supported on mobiles, tablets, and foldable devices running Android 9 (API level 28) and higher. The minimum version of Google Play services (GMS) required is 25.49.x. +This feature is supported on mobiles, tablets, and foldable devices running +Android 9 (API level 28) and higher. The minimum version of Google Play services +(GMS) required is 25.49.x. ## User experience -The following sections describe the user experience during the verification flow, the need to include fallback verification methods, as well as the recommended user experience for various use cases. +The following sections describe the user experience during the verification +flow, the need to include fallback verification methods, as well as the +recommended user experience for various use cases. ### The verification flow The user experience for sharing a verified email is as follows: -1. The user either focuses on an input field or taps a button that calls the Credential Manager API. Depending on the design of the screen, you can also call the API on your app's screen load. +1. The user either focuses on an input field or taps a button that calls the + Credential Manager API. Depending on the design of the screen, you can also + call the API on your app's screen load. -2. A bottom sheet appears, showing the information that will be shared with the app. If no information is available on that device, the user sees a generic error message. +2. A bottom sheet appears, showing the information that will be shared with the + app. If no information is available on that device, the user sees a generic + error message. -3. After the user taps **Agree and Continue**, display a success or failure message. +3. After the user taps **Agree and Continue**, display a success or failure + message. > [!NOTE] > **Note:** If the verified email you receive does not match what you expect, inform the user about the mismatch and either ask them to try again with a different credential or provide an alternate verification method, such as through OTPs. -4. (Optional, recommended) If the user is signing up for your service, you should prompt the user to [create](https://developer.android.com/identity/passkeys/create-passkeys) a [passkey](https://developer.android.com/identity/passkeys) to make it easier for them to sign in subsequently. +4. (Optional, recommended) If the user is signing up for your service, you + should prompt the user to [create](https://developer.android.com/identity/passkeys/create-passkeys) a [passkey](https://developer.android.com/identity/passkeys) to make it easier for + them to sign in subsequently. > [!NOTE] > **Note:** The email verification process doesn't automatically trigger passkey creation. However, it is highly recommended to include the steps for passkey creation. Passkeys help users by making it easier and more secure for them to sign in, and remove the need for the conventional username and password interaction. ### Include primary and fallback flows -To ensure a streamlined user experience, include the following options on screens that require email verification: +To ensure a streamlined user experience, include the following options on +screens that require email verification: - **Primary verification option**: An email field or button to trigger the Credential Manager API flow for quick verification. - **Alternate verification options**: A link or button for users to "Verify another way" or with "Other options" for manual email entry in case of failures, such as no information available on the device, or a mismatch between the retrieved and expected email. This should allow users to try verification with a different credential or by providing a manual OTP. ### Use cases -The following sections describe the recommended use cases, as well as the suggested user experience, for email verification. +The following sections describe the recommended use cases, as well as the +suggested user experience, for email verification. #### Sign up -Users can immediately create an account with a verified email without a separate verification step. Optionally, prompt the user to add a passkey. If they opt to add a passkey, trigger the [passkey creation](https://developer.android.com/identity/passkeys/create-passkeys) flow. +Users can immediately create an account with a verified email without a separate +verification step. Optionally, prompt the user to add a passkey. If they opt to +add a passkey, trigger the [passkey creation](https://developer.android.com/identity/passkeys/create-passkeys) flow. ![Using email verification during sign up, and then creating passkeys](https://developer.android.com/static/identity/digital-credentials/images/signup_ux.png) Email verification during sign up #### Account recovery -To eliminate the frustration of users searching for recovery codes in their spam folders, allow them to recover their account using the verified email securely stored on their device. Additionally, suggest that they create a passkey for future use. +To eliminate the frustration of users searching for recovery codes in their spam +folders, allow them to recover their account using the verified email securely +stored on their device. Additionally, suggest that they create a passkey for +future use. ![Using email verification during account recovery](https://developer.android.com/static/identity/digital-credentials/images/account_recovery_ux.png) Email verification during account recovery #### Reauthentication for sensitive actions -Protect sensitive user actions, such as changing settings or updating profile details, by requiring a quick reauthentication step. +Protect sensitive user actions, such as changing settings or updating profile +details, by requiring a quick reauthentication step. ![Using email verification during reauthentication](https://developer.android.com/static/identity/digital-credentials/images/reauthentication_ux.png) Email verification during reauthentication ## Accounts supported -Email verification through Credential Manager only supports verification of consumer Google Accounts. [Workspace accounts](https://knowledge.workspace.google.com/admin/getting-started/set-up-google-workspace-for-your-organization) and [supervised accounts](https://support.google.com/families/answer/9499054) are not supported. +Email verification through Credential Manager only supports verification of +consumer Google Accounts. [Workspace accounts](https://knowledge.workspace.google.com/admin/getting-started/set-up-google-workspace-for-your-organization) and [supervised +accounts](https://support.google.com/families/answer/9499054) are not supported. -A consumer Google Account can be created with an email address from any provider, not necessarily @gmail.com. However, Google verifies these accounts differently: +A consumer Google Account can be created with an email address from any +provider, not necessarily @gmail.com. However, Google verifies these accounts +differently: - For @gmail.com accounts: Google is the authoritative source, and the email is known to be verified. - For non-@gmail.com accounts: Google is not the authoritative source for these email addresses in the long term. While Google verifies the email when the account is created, the ownership of that email address might change over time. Therefore, for non-@gmail.com addresses, you should consider an additional verification step, such as sending an OTP, to ensure that the user still has access to the email account. -For more information about what verification implies, see [Digital Credentials](https://developer.android.com/identity/digital-credentials#verified). +For more information about what verification implies, see [Digital +Credentials](https://developer.android.com/identity/digital-credentials#verified). > [!NOTE] > **Note:** Apart from a user's email information, you can request other unverified fields, such as the user's given name, family name, name, and the profile picture of their Google Account. However, only the email is verified by Google. @@ -83,19 +110,30 @@ For more information about what verification implies, see [Digital Credentials]( ## Validity and freshness -The system issues [verifiable credentials](https://developer.android.com/identity/digital-credentials#verified) (VCs) based on the user's current email from the active Google Accounts on the device. These credentials are issued to the device in advance, typically while the device is idle. While these credentials might remain valid for multiple days, the system performs a check at the moment of sharing the credentials to ensure that the account still exists, is on the device, and that the email address is valid---effectively prioritizing the account's immediate status over the credential's validity window. +The system issues [verifiable credentials](https://developer.android.com/identity/digital-credentials#verified) (VCs) based on the user's current +email from the active Google Accounts on the device. These credentials are +issued to the device in advance, typically while the device is idle. While these +credentials might remain valid for multiple days, the system performs a check at +the moment of sharing the credentials to ensure that the account still exists, +is on the device, and that the email address is valid---effectively prioritizing +the account's immediate status over the credential's validity window. -To help ensure authenticity, a Key Binding (kb) signature is generated at the time of sharing, incorporating the nonce. +To help ensure authenticity, a Key Binding (kb) signature is generated at the +time of sharing, incorporating the nonce. -If a device is offline or the account is removed, the process fails rather than providing an expired VC or a VC for an inactive Google Account. +If a device is offline or the account is removed, the process fails rather than +providing an expired VC or a VC for an inactive Google Account. ### Email deliverability -While the process confirms the account's legitimacy, it does not guarantee inbox delivery (for instance, the email might be diverted to spam). An OTP remains the definitive method for confirming email deliverability. +While the process confirms the account's legitimacy, it does not guarantee inbox +delivery (for instance, the email might be diverted to spam). An OTP remains the +definitive method for confirming email deliverability. ## Comparison with Sign in with Google -While both Digital Credentials and [Sign in with Google](https://developer.android.com/identity/sign-in/credential-manager-siwg) solutions provide a verified email, the user flows and use cases are different: +While both Digital Credentials and [Sign in with Google](https://developer.android.com/identity/sign-in/credential-manager-siwg) solutions provide a +verified email, the user flows and use cases are different: - **Use cases**: The Credential Manager email verification flow is not exclusively used in sign up or sign in use cases, but rather can be used in any use case involving the retrieval of verified email. This could include account recovery as well. - **Registration**: The Credential Manager flow does not require Google registration, unlike Sign in with Google. diff --git a/identity/verified-email/references/android/identity/digital-credentials/index.md b/identity/verified-email/references/android/identity/digital-credentials/index.md index 0663d39..5f9ca67 100644 --- a/identity/verified-email/references/android/identity/digital-credentials/index.md +++ b/identity/verified-email/references/android/identity/digital-credentials/index.md @@ -1,20 +1,32 @@ -Digital credentials are cryptographically verifiable documents that can be used to authenticate, authorize, or otherwise provide information about a user. These are typically things such as mobile driver's licenses, digital passports, boarding passes, etc. They reside in virtual containers called digital wallets, and are part of a W3C standard that specifies how to access and retrieve them. This standard is implemented for web use cases with the [W3C Credential Management API](https://www.w3.org/TR/credential-management-1/) and on Android, with Credential Manager's [DigitalCredential API](https://developer.android.com/reference/kotlin/androidx/credentials/DigitalCredential). +Digital credentials are cryptographically verifiable documents that can be used +to authenticate, authorize, or otherwise provide information about a user. These +are typically things such as mobile driver's licenses, digital passports, +boarding passes, etc. They reside in virtual containers called digital wallets, +and are part of a W3C standard that specifies how to access and retrieve them. +This standard is implemented for web use cases with the [W3C Credential +Management API](https://www.w3.org/TR/credential-management-1/) and on Android, with Credential Manager's +[DigitalCredential API](https://developer.android.com/reference/kotlin/androidx/credentials/DigitalCredential). ## Understand digital credentials -In the physical world, a person might keep their identity in their wallet, and present it to a requesting party when asked: +In the physical world, a person might keep their identity in their wallet, and +present it to a requesting party when asked: ![Image showing the flow of a normal wallet interaction](https://developer.android.com/static/identity/digital-credentials/images/normal_wallet_flowchart.svg) **Figure 1.** The process of fulfilling a physical-world credential request. The requestor asks the user for a specific credential. Then, the user selects and retrieves it from their physical wallet. Finally, the user provides the credential to the requestor. -In this case, a user generally has a single wallet, and retrieves the requested credentials from the wallet to present to the requestor. Wallets are mostly interchangeable, and can generally store the same things. +In this case, a user generally has a single wallet, and retrieves the requested +credentials from the wallet to present to the requestor. Wallets are mostly +interchangeable, and can generally store the same things. -Digital credentials have the following differences from credentials in the physical world: +Digital credentials have the following differences from credentials in the +physical world: 1. Users are expected to have multiple wallets - also known as **holders** - which can contain various different credentials. Wallets determine which credentials may be stored inside of them. 2. The app or service asking for the credential to grant access or verify an identity is called the **verifier**. 3. The entity that creates the credential and asserts claims about the subject (such as, a university, a government, or a tech company) is referred to as the **issuer**. 4. The credential presentation happens in software, which means an API surface retrieves and presents the credentials - in Android, this is Credential Manager. -As such, Credential Manager takes on several roles that were formerly handled by the user: +As such, Credential Manager takes on several roles that were formerly handled by +the user: 1. On Android, wallets must register their credentials metadata with Credential Manager to be listed in the Credential Manager UI. 2. Credential Manager matches credentials across wallets based on the request and presents a list for the user to select. @@ -25,32 +37,52 @@ This flow is shown here: ## Verifiable credentials -Verifiable credentials are a subset of digital credentials governed by strict standards (like the W3C Verifiable Credentials Data Model). These credentials contain claims that are cryptographically secured, making them tamper-evident and proving exactly who issued them. +Verifiable credentials are a subset of digital credentials governed by strict +standards (like the W3C Verifiable Credentials Data Model). These credentials +contain claims that are cryptographically secured, making them tamper-evident +and proving exactly who issued them. -Not all digital credentials are verifiable credentials, but all verifiable credentials are digital credentials. +Not all digital credentials are verifiable credentials, but all verifiable +credentials are digital credentials. ## What it means for a claim to be verified -When a credential arrives through the Android Credential Manager API and a claim within it is marked as "verified," it implies that the issuer is asserting that they performed a check on that specific piece of data. However, it does not mean the data is an absolute, universal truth. "Verified" is an assertion of process, not an automatic guarantee of trust. +When a credential arrives through the Android Credential Manager API and a claim +within it is marked as "verified," it implies that the issuer is asserting that +they performed a check on that specific piece of data. However, it does not mean +the data is an absolute, universal truth. "Verified" is an assertion of process, +not an automatic guarantee of trust. -The core philosophy of this ecosystem is that trust is always resolved at the verifier. When the verifier (your app) receives the cryptographically secure data, and sees that the issuer marked it as "verified," it must determine whether it trusts the issuer to have verified the claim to its standards. +The core philosophy of this ecosystem is that trust is always resolved at the +verifier. When the verifier (your app) receives the cryptographically secure +data, and sees that the issuer marked it as "verified," it must determine +whether it trusts the issuer to have verified the claim to its standards. ### User experience -As shown in the Android flow, the user only needs to interact once with the Credential Manager UI to select the appropriate credential. Here is an example of how the selector looks: +As shown in the Android flow, the user only needs to interact once with the +Credential Manager UI to select the appropriate credential. Here is an example +of how the selector looks: ![Image showing the digital credentials UI in Credential Manager](https://developer.android.com/static/identity/digital-credentials/images/digital_credentials_ui.png) **Figure 3.** The digital credentials UI. ### Standards -Digital credentials requests are created using the [OpenID4VP standard](https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#name-introduction). You can see example requests at the [Digital Credentials Demo site](https://digital-credentials.dev/). +Digital credentials requests are created using the [OpenID4VP +standard](https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#name-introduction). You can see example requests at the [Digital +Credentials Demo site](https://digital-credentials.dev/). -Digital credential responses are typically returned in a standardized credential format. These are maintained by different standards bodies, and include [W3C Verifiable Credentials](https://www.w3.org/TR/vc-data-model-2.0/), [sd-jwt](https://datatracker.ietf.org/doc/draft-ietf-oauth-selective-disclosure-jwt/), and [mdoc](https://www.iso.org/standard/69084.html). +Digital credential responses are typically returned in a standardized credential +format. These are maintained by different standards bodies, and include [W3C +Verifiable Credentials](https://www.w3.org/TR/vc-data-model-2.0/), [sd-jwt](https://datatracker.ietf.org/doc/draft-ietf-oauth-selective-disclosure-jwt/), and +[mdoc](https://www.iso.org/standard/69084.html). -Custom protocols are also feasible, though we recommend using one of the standard protocols in your application. +Custom protocols are also feasible, though we recommend using one of the +standard protocols in your application. ### Try it out -You can test out the digital credentials flow across platforms with an Android wallet and web-based verifier: +You can test out the digital credentials flow across platforms with an Android +wallet and web-based verifier: 1. Install the [CMWallet public sample](https://github.com/digitalcredentialsdev/CMWallet) on your Android phone. You can do this by pulling from the repository and installing directly from Android Studio or navigating to and selecting the latest build to access the latest `app-debug.apk` file. 2. Open the CMWallet to register the metadata with Credential Manager. Make sure Bluetooth is enabled to allow your devices to connect to each other. diff --git a/identity/verified-email/references/android/identity/passkeys/create-passkeys.md b/identity/verified-email/references/android/identity/passkeys/create-passkeys.md index 3a190a3..089cc21 100644 --- a/identity/verified-email/references/android/identity/passkeys/create-passkeys.md +++ b/identity/verified-email/references/android/identity/passkeys/create-passkeys.md @@ -1,6 +1,11 @@ -Before your users can authenticate with passkeys, your app must first register or create the passkey for their account. +Before your users can authenticate with passkeys, your app must first register +or create the passkey for their account. -To create the passkey, obtain details required to create the passkey from your app server, and then call the Credential Manager API, which returns a public and private key pair. The returned private key is stored in a credential provider, such as Google Password Manager, as a passkey. The public key is stored on your app server. +To create the passkey, obtain details required to create the passkey from your +app server, and then call the Credential Manager API, which returns a public and +private key pair. The returned private key is stored in a credential provider, +such as Google Password Manager, as a passkey. The public key is stored on your +app server. > [!TIP] > **Tip:** While designing authentication flows with passkeys, make sure that you follow the [UX guidelines for passkeys](https://developer.android.com/design/ui/mobile/guides/patterns/passkeys). @@ -9,11 +14,15 @@ To create the passkey, obtain details required to create the passkey from your a ## Prerequisites -Make sure that you have set up [Digital Asset Links](https://developer.android.com/identity/credential-manager/prerequisites) and that you target devices running Android 9 (API level 28) or higher. +Make sure that you have set up [Digital Asset Links](https://developer.android.com/identity/credential-manager/prerequisites) and that you target +devices running Android 9 (API level 28) or higher. ## Overview -This guide focuses on the changes required in your [relying party client app](https://developer.android.com/identity/credential-manager#authentication-terminology) to create a passkey, and gives a brief overview of the [relying party app server](https://developer.android.com/identity/credential-manager#authentication-terminology) implementation. To learn more about the server-side integration, see [Server-side passkey registration](https://developers.google.com/identity/passkeys/developer-guides/server-registration). +This guide focuses on the changes required in your [relying party client app](https://developer.android.com/identity/credential-manager#authentication-terminology) +to create a passkey, and gives a brief overview of the [relying party app +server](https://developer.android.com/identity/credential-manager#authentication-terminology) implementation. To learn more about the server-side integration, see +[Server-side passkey registration](https://developers.google.com/identity/passkeys/developer-guides/server-registration). 1. [**Add dependencies to your app**](https://developer.android.com/identity/passkeys/create-passkeys#add-dependencies): Add the required Credential Manager libraries. 2. [**Instantiate Credential Manager**](https://developer.android.com/identity/passkeys/create-passkeys#instantiate): Create a Credential Manager instance. @@ -24,7 +33,8 @@ This guide focuses on the changes required in your [relying party client app](ht 7. [**Notify the user**](https://developer.android.com/identity/passkeys/create-passkeys#notify): Notify the user that their passkey has been created. > [!TIP] -> **Tip:** To enhance the user experience during sign up, add functionality to [restore credentials](https://developer.android.com/identity/sign-in/restore-credentials) on a new device to let users seamlessly set up their existing accounts on new Android devices, and [suppressing autofill dialogs on credential fields](https://developer.android.com/identity/passkeys/create-passkeys#suppress-autofill). +> **Tip:** To enhance the user experience during sign up, add functionality to [restore credentials](https://developer.android.com/identity/sign-in/restore-credentials) on a new device to let users seamlessly set up their existing accounts on new Android devices, and [suppressing autofill dialogs on +> credential fields](https://developer.android.com/identity/passkeys/create-passkeys#suppress-autofill). ## Add dependencies to your app @@ -61,16 +71,26 @@ Use your app or activity context to create a `CredentialManager` object. ## Get credential creation options from your app server -When the user clicks a "Create Passkey" button or when a new user signs up, make a request from your app to your app server to obtain the information required to start the passkey registration process. +When the user clicks a "Create Passkey" button or when a new user signs up, make +a request from your app to your app server to obtain the information required to +start the passkey registration process. -Use a FIDO-compliant library in your app server to send your client app the information required to create a passkey, such as information about the user, the app, and additional configuration properties. To learn more, see [Server side passkey registration](https://developers.google.com/identity/passkeys/developer-guides/server-registration). +Use a FIDO-compliant library in your app server to send your client app the +information required to create a passkey, such as information about the user, +the app, and additional configuration properties. To learn more, see [Server +side passkey registration](https://developers.google.com/identity/passkeys/developer-guides/server-registration). -In the client app, decode the public key creation options sent by the app server. These are usually represented in JSON format. To learn more about how this decoding is done for web clients, see [Encoding and Decoding](https://developers.google.com/identity/passkeys/developer-guides/server-registration#encoding_and_decoding). For Android client apps, you must handle the decoding separately. +In the client app, decode the public key creation options sent by the app +server. These are usually represented in JSON format. To learn more about how +this decoding is done for web clients, see [Encoding and +Decoding](https://developers.google.com/identity/passkeys/developer-guides/server-registration#encoding_and_decoding). For Android client apps, you must handle the decoding +separately. > [!NOTE] > **Note:** On your app server, securely store the `challenge` so that you can later verify the origin of the credential. -The following snippet shows the structure the public key creation options sent by the app server: +The following snippet shows the structure the public key creation options sent +by the app server: { "challenge": "", @@ -129,7 +149,9 @@ Key fields in the public key creation options include: ## Create a passkey -After you have parsed the server-side public key creation options, create a passkey by wrapping these options in a `CreatePublicKeyCredentialRequest` object and calling `createCredential()`. +After you have parsed the server-side public key creation options, create a +passkey by wrapping these options in a `CreatePublicKeyCredentialRequest` object +and calling `createCredential()`. The `createPublicKeyCredentialRequest` includes the following: @@ -138,14 +160,25 @@ The `createPublicKeyCredentialRequest` includes the following: - `false` (default): Use this value if the call to Credential Manager was triggered by an explicit user action. - `true`: Use this value if Credential Manager is opportunistically called, such as when first opening the app. If you set the value to `true` and there are no immediately available credentials, Credential Manager won't show any UI and the request will fail immediately, returning NoCredentialException for get requests and [`CreateCredentialNoCreateOptionException`](https://developer.android.com/reference/kotlin/androidx/credentials/exceptions/CreateCredentialNoCreateOptionException) for create requests. -- `origin`: This field is automatically set for Android apps. For browsers and similarly privileged apps that need to set `origin`, see [Make Credential Manager calls on behalf of other parties for privileged apps](https://developer.android.com/training/sign-in/privileged-apps). +- `origin`: This field is automatically set for Android apps. For browsers and similarly privileged apps that need to set `origin`, see [Make Credential + Manager calls on behalf of other parties for privileged apps](https://developer.android.com/training/sign-in/privileged-apps). - `isConditional`: This is an optional field that defaults to `false`. For more information, see [Automatically create a passkey](https://developer.android.com/identity/passkeys/create-passkeys#automatic-upgrade). -Calling the `createCredential()` function launches Credential Manager's built-in bottom sheet UI that prompts the user to use a passkey and to select a credential provider and account for storage. However, if `isConditional` is set to `true`, the bottom sheet UI does not display, and the passkey is automatically created. +Calling the `createCredential()` function launches Credential Manager's built-in +bottom sheet UI that prompts the user to use a passkey and to select a +credential provider and account for storage. However, if `isConditional` is set +to `true`, the bottom sheet UI does not display, and the passkey is +automatically created. ### Automatically create a passkey -You can automatically create a passkey for a user after a successful password login by setting the `isConditional` parameter to `true` in your `CreatePublicKeyCredentialRequest` while creating a passkey. If the user doesn't already have a passkey, your app will automatically attempt to create one in the background and store it in the user's credential provider, such as Google Password Manager. For an example of how this is implemented, see the [public sample](https://github.com/android/identity-samples/blob/main/Shrine/app/src/main/java/com/authentication/shrine/ui/AuthenticationScreen.kt#L98). +You can automatically create a passkey for a user after a successful password +login by setting the `isConditional` parameter to `true` in your +`CreatePublicKeyCredentialRequest` while creating a passkey. If the user doesn't +already have a passkey, your app will automatically attempt to create one in the +background and store it in the user's credential provider, such as Google +Password Manager. For an example of how this is implemented, see the [public +sample](https://github.com/android/identity-samples/blob/main/Shrine/app/src/main/java/com/authentication/shrine/ui/AuthenticationScreen.kt#L98). ![An example of the notification Google Password Manager shows after passkey creation](https://developer.android.com/static/identity/passkeys/images/conditional-create-gpm.svg) **Figure 2:**Google Password Manager notification > [!NOTE] @@ -153,9 +186,11 @@ You can automatically create a passkey for a user after a successful password lo ## Handle the response -After the user is verified using the device's screen lock, a passkey is created and stored in the user's selected credential provider. +After the user is verified using the device's screen lock, a passkey is created +and stored in the user's selected credential provider. -The response after you successfully call `createCredential()` is a [PublicKeyCredential](https://developer.android.com/jetpack/androidx/releases/credentials) object. +The response after you successfully call `createCredential()` is a +[PublicKeyCredential](https://developer.android.com/jetpack/androidx/releases/credentials) object. The `PublicKeyCredential` looks as follows: @@ -206,55 +241,79 @@ Add code to handle failures as shown in the following snippet: ## Verify and save the public key on the app server -On the app server, you must verify the public key credential and then [save the public key](https://web.dev/articles/passkey-registration#save-credential). +On the app server, you must verify the public key credential and then [save the +public key](https://web.dev/articles/passkey-registration#save-credential). -To verify the public key credential's origin, compare it against an allow list of approved apps. If a key has an unrecognized origin, reject it. +To verify the public key credential's origin, compare it against an allow list +of approved apps. If a key has an unrecognized origin, reject it. > [!NOTE] > **Note:** An app's origin is based on its unique identity, which is the SHA-256 fingerprint of its signing certificate. To obtain the app's SHA 256 fingerprint: -1. Print your release app's signing certificate by running the following command in a terminal: +1. Print your release app's signing certificate by running the following + command in a terminal: keytool -list -keystore - In the response, identify the signing certificate's SHA 256 fingerprint, mentioned as `Certificate fingerprints block` : `SHA256`. -2. Encode the SHA256 fingerprint with base64url encoding. This Python example demonstrates how to properly encode the fingerprint: + In the response, identify the signing certificate's SHA 256 fingerprint, + mentioned as `Certificate fingerprints block` : `SHA256`. +2. Encode the SHA256 fingerprint with base64url encoding. This Python example + demonstrates how to properly encode the fingerprint: import binascii import base64 fingerprint = '' # your app's SHA256 fingerprint print(base64.urlsafe_b64encode(binascii.a2b_hex(fingerprint.replace(':', ''))).decode('utf8').replace('=', '')) -3. Append `android:apk-key-hash`: to the start of the output from the previous step so that you get something that is similar to the following: +3. Append `android:apk-key-hash`: to the start of the output from the previous + step so that you get something that is similar to the following: android:apk-key-hash: - The result should match with an allowed origin on your app server. If you have multiple signing certificates, such as certificates for debugging and release, or multiple apps, then repeat the process and accept all the origins as valid on the app server. + The result should match with an allowed origin on your app server. If you + have multiple signing certificates, such as certificates for debugging and + release, or multiple apps, then repeat the process and accept all the + origins as valid on the app server. > [!NOTE] > **Note:** When you save the passkey on the app server, make sure that you save the Authenticator Attestation Globally Unique Identifier ([AAGUID](https://web.dev/articles/webauthn-aaguid)) from the client data. The AAGUID is a unique number that identifies the model of the authenticator. For more information, see [Manage passkeys](https://developer.android.com/identity/passkeys/manage-passkeys). ## Notify the user -After the passkey is successfully created, notify your users about the passkey and inform them that they can manage their passkeys from their credential provider app or from [within the app settings](https://developer.android.com/identity/passkeys/manage-passkeys). Notify users by using a custom dialog, notification, or snackbar. Since an unexpected passkey creation by a malicious entity requires an immediate security alert, consider supplementing these in-app methods with external communication, such as an email. +After the passkey is successfully created, notify your users about the passkey +and inform them that they can manage their passkeys from their credential +provider app or from [within the app settings](https://developer.android.com/identity/passkeys/manage-passkeys). Notify users by using a +custom dialog, notification, or snackbar. Since an unexpected passkey creation +by a malicious entity requires an immediate security alert, consider +supplementing these in-app methods with external communication, such as an +email. ## Enhance the user experience -To enhance the user experience while implementing sign up with Credential Manager, consider adding functionality for restore credentials and suppress autofill dialogs. +To enhance the user experience while implementing sign up with Credential +Manager, consider adding functionality for restore credentials and suppress +autofill dialogs. ### Add functionality to restore credentials on a new device -To allow users to seamlessly log into their accounts on a new device, implement the [Restore Credentials](https://developer.android.com/identity/sign-in/restore-credentials) functionality. Adding restore credentials with `BackupAgent` logs users in when they open your restored app on a new device, letting them use your app right away. +To allow users to seamlessly log into their accounts on a new device, implement +the [Restore Credentials](https://developer.android.com/identity/sign-in/restore-credentials) functionality. Adding restore credentials with +`BackupAgent` logs users in when they open your restored app on a new device, +letting them use your app right away. ### Suppress autofill on credential fields (optional) -For app screens where users are expected to use Credential Manager's bottom sheet UI for authentication, add the `isCredential` attribute to the username and password fields. This suppresses autofill dialogs (`FillDialog` and `SaveDialog`) from overlapping with Credential Manager's bottom sheet UI. +For app screens where users are expected to use Credential Manager's bottom +sheet UI for authentication, add the `isCredential` attribute to the username +and password fields. This suppresses autofill dialogs (`FillDialog` and +`SaveDialog`) from overlapping with Credential Manager's bottom sheet UI. The `isCredential` attribute is supported on Android 14 and higher. -The following example demonstrates how you can add the `isCredential` attribute to the relevant username and password fields in the relevant views for your app: +The following example demonstrates how you can add the `isCredential` attribute +to the relevant username and password fields in the relevant views for your app: ```kotlin @Preview(name = "Phone", device = Devices.PHONE, showBackground = true) @@ -61,21 +64,25 @@ fun FeedScreenPreview() { } } } - ```
## Step 2. Make the navigation bar adaptive -Bottom navigation bars are optimized for touch input when the user is holding a phone in portrait mode. On larger screen hand-held devices, like tablets and unfolded foldables, the navigation area must be accessible from the edge of the screen (navigation rail). +Bottom navigation bars are optimized for touch input when the user is holding a +phone in portrait mode. On larger screen hand-held devices, like tablets and +unfolded foldables, the navigation area must be accessible from the edge of the +screen (navigation rail). -If you need to provide more screen space for the content, hide the navigation area. Examples of this include: +If you need to provide more screen space for the content, hide the +navigation area. Examples of this include: - Hiding the navigation bar when the user scrolls down and showing it again when the user scrolls up. The assumption is that when the user is scrolling down, they are consuming content but when scrolling up they are trying to navigate away from that content. - Hiding the navigation area when its content is distracting. For example, in camera previews or when displaying a full-screen photo. -When the detail screen is displayed full-screen on mobile, full-screen mode must be deactivated on larger screens. +When the detail screen is displayed full-screen on mobile, full-screen mode must +be deactivated on larger screens. Steps to migrate: @@ -87,7 +94,10 @@ Steps to migrate: ### Step 2.1. Control navigation area visibility -If the navigation bar's visibility changes - it is hidden under certain scenarios or on certain screens - this behavior must be maintained with the adaptive navigation area. This is done using `NavigationSuiteScaffold`'s `state` parameter. +If the navigation bar's visibility changes - it is hidden under certain +scenarios or on certain screens - this behavior must be maintained with the +adaptive navigation area. This is done using `NavigationSuiteScaffold`'s `state` +parameter. Steps to migrate: @@ -97,7 +107,6 @@ Steps to migrate: For example: -
```kotlin // Pass this variable to any composable that needs to control the navigation area visibility @@ -118,26 +127,33 @@ LaunchedEffect(isNavBarVisible){ scaffoldVisibilityState.hide() } } - ```
## Step 3. Add multi-pane layouts using Navigation 3 Scenes -Analyze the codebase looking for related screens - tapping on something in one screen opens another screen that shows information related to the first. There are two canonical screen relationships: list-detail and supporting pane. +Analyze the codebase looking for related screens - tapping on something in one +screen opens another screen that shows information related to the first. There +are two canonical screen relationships: list-detail and supporting pane. -IMPORTANT: You must use the Navigation 3 `SceneStrategy` approach to implement multi-pane layouts. Do not use `ListDetailPaneScaffold` or `SupportingPaneScaffold`. +IMPORTANT: You must use the Navigation 3 `SceneStrategy` approach to implement +multi-pane layouts. Do not use `ListDetailPaneScaffold` or +`SupportingPaneScaffold`. ### Step 3.1. List-detail #### Identify the list and detail screens -List-detail layouts display a list of items (this is the list screen) and clicking on an item opens a new screen that shows more details about that item (the detail screen). +List-detail layouts display a list of items (this is the list screen) and +clicking on an item opens a new screen that shows more details about that item +(the detail screen). Typical usage includes productivity apps like email, notes, and messaging. -Unless requested explicitly, avoid this pattern when the detail content requires substantial screen space (e.g., images or media that benefits from a full-screen presentation). +Unless requested explicitly, avoid this pattern when the detail content requires +substantial screen space (e.g., images or media that benefits from a full-screen +presentation). #### Add a Material list-detail SceneStrategy @@ -147,7 +163,8 @@ Unless requested explicitly, avoid this pattern when the detail content requires #### Use metadata to identify the list and detail screens -- Add metadata using `entry(metadata = ...)` or `NavEntry(metadata = ...)` to the list entry using `ListDetailSceneStrategy.listPane(detailPlaceholder = { })`. +- Add metadata using `entry(metadata = ...)` or `NavEntry(metadata = ...)` to the list entry using `ListDetailSceneStrategy.listPane(detailPlaceholder = { + })`. - Use the `detailPlaceholder` parameter to add a placeholder on the detail screen when no list items are selected. - Add metadata to the detail entry using `ListDetailSceneStrategy.detailPane()`. @@ -156,11 +173,14 @@ Unless requested explicitly, avoid this pattern when the detail content requires - When a detail screen displays its content full-screen on mobile (content fills the entire screen, bars or rails are hidden), full-screen mode must be deactivated if it's part of a list-detail layout. - Detail screens must not show a back arrow when on a list-detail layout. -For a reference implementation, check the [Nav3 **Material** List Detail recipe](references/android/guide/navigation/navigation-3/recipes/material-listdetail.md). +For a reference implementation, check the [Nav3 **Material** List Detail +recipe](references/android/guide/navigation/navigation-3/recipes/material-listdetail.md). ### Step 3.2. Supporting pane -Identify supporting pane screens where a main screen displays a single item, and selecting it opens a "supporting screen" with more details. The supporting screen complements the main screen and is shown in a supporting pane. +Identify supporting pane screens where a main screen displays a single item, and +selecting it opens a "supporting screen" with more details. The supporting +screen complements the main screen and is shown in a supporting pane. #### Add a Material supporting pane `SceneStrategy` @@ -175,13 +195,15 @@ Identify supporting pane screens where a main screen displays a single item, and ### Step 3.3. Run screenshot tests -If you have made changes, record new reference files. Ask the user to visually verify that the new layouts are correct. +If you have made changes, record new reference files. Ask the user to visually +verify that the new layouts are correct. ## Step 4. Make vertical lists adaptive by changing the number of columns ### Step 4.1. Make lazy lists adaptive -Look for the following vertical list composables: `LazyColumn`, `LazyVerticalGrid`, `LazyVerticalStaggeredGrid`. +Look for the following vertical list composables: `LazyColumn`, +`LazyVerticalGrid`, `LazyVerticalStaggeredGrid`. Steps to migrate: @@ -192,16 +214,24 @@ Steps to migrate: ### Step 4.2. Migrate non-lazy lists to Grid -WARNING: Grid is an experimental API available from Compose 1.11.0-beta01. Confirm with the user that they are happy to use an experimental API in their codebase. +WARNING: Grid is an experimental API available from Compose 1.11.0-beta01. +Confirm with the user that they are happy to use an experimental API in their +codebase. -Look for any `Column` that contains multiple items of the same type and replace it with `Grid`. Do not replace it with `LazyVerticalGrid` or any other lazy layout. Do not place `Grid` inside the existing `Column`. Completely replace it. +Look for any `Column` that contains multiple items of the same type and replace +it with `Grid`. Do not replace it with `LazyVerticalGrid` or any other lazy +layout. Do not place `Grid` inside the existing `Column`. Completely replace it. -`Grid` is configured by supplying a lambda (an extension function on `GridConfigurationScope`) to its `config` parameter. Inside the lambda, `constraints` provides the minimum and maximum dimensions of the grid container and can be used to change the number of rows and columns based on the available size. For example, the following code configures `Grid` such that when the available width is: +`Grid` is configured by supplying a lambda (an extension function on +`GridConfigurationScope`) to its `config` parameter. Inside the lambda, +`constraints` provides the minimum and maximum dimensions of the grid container +and can be used to change the number of rows and columns based on the available +size. For example, the following code configures `Grid` such that when the +available width is: - less than 800dp, a 2x4 grid is used - 800dp or more, a 4x2 grid is used -
```kotlin Grid( @@ -220,23 +250,26 @@ Grid( gap(gapSizeDp) } ) { /** items **/ } - ```
-`Grid` is an experimental API so add the `@OptIn(ExperimentalGridApi::class)` annotation to any function that uses it. +`Grid` is an experimental API so add the `@OptIn(ExperimentalGridApi::class)` +annotation to any function that uses it. ## Step 5: Hide App Bars when scrolling -In an app with multiple top-level destinations, each screen must manage its own app bar state independently. There are two main scroll behaviors: +In an app with multiple top-level destinations, each screen must manage its own +app bar state independently. There are two main scroll behaviors: - `exitUntilCollapsedScrollBehavior`: Hides on scroll down, stays hidden while you scroll up until you reach the very top (0 offset). - `enterAlwaysScrollBehavior`: Hides on scroll down, shows immediately on scroll up. ## Final step: Build and test -Build the app and run the local tests. If the project has screenshot tests, run them but DO NOT update the reference images. Prompt the user to do this after they have viewed the screenshot diffs. +Build the app and run the local tests. If the project has screenshot tests, run +them but DO NOT update the reference images. Prompt the user to do this after +they have viewed the screenshot diffs. ## Additional documentation for experimental adaptive APIs @@ -253,11 +286,14 @@ Check the FlexBox documentation: ## MediaQuery -Check the [MediaQuery documentation](references/android/develop/ui/compose/layouts/adaptive/mediaquery/index.md) when you need to query the device's screen size, pointer precision, keyboard type, whether it has cameras or microphones, and other device capabilities. +Check the [MediaQuery documentation](references/android/develop/ui/compose/layouts/adaptive/mediaquery/index.md) when you need to query the device's +screen size, pointer precision, keyboard type, whether it has cameras or +microphones, and other device capabilities. ## Grid -Check the Grid documentation when you need to display a fixed number of items in a grid layout: +Check the Grid documentation when you need to display a fixed number of items in +a grid layout: - [Overview](references/android/develop/ui/compose/layouts/adaptive/grid/index.md) - [Get started - setup](references/android/develop/ui/compose/layouts/adaptive/grid/get-started.md) diff --git a/jetpack-compose/adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/container-behavior.md b/jetpack-compose/adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/container-behavior.md index 27c2e3d..41112fc 100644 --- a/jetpack-compose/adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/container-behavior.md +++ b/jetpack-compose/adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/container-behavior.md @@ -1,6 +1,6 @@ -To configure the behavior of the `FlexBox` container, create a `FlexBoxConfig` block and supply it using the `config` parameter. +To configure the behavior of the `FlexBox` container, create a `FlexBoxConfig` +block and supply it using the `config` parameter. -
```kotlin FlexBox( @@ -14,16 +14,17 @@ FlexBox( } ) { // child items } - ```
-Use `FlexBoxConfig` to define the layout direction, wrapping behavior, alignment, and gaps between items. +Use `FlexBoxConfig` to define the layout direction, wrapping behavior, +alignment, and gaps between items. ## Layout direction -The `direction` function sets the main axis, which dictates the direction items are laid out in. It accepts the following values: +The `direction` function sets the main axis, which dictates the direction +items are laid out in. It accepts the following values: - `Row` (default): Sets the main axis to be horizontal. In left-to-right locales this will be left-to-right, with the opposite in right-to-left. - `RowReverse`: Reverses the direction of `Row`. @@ -32,11 +33,13 @@ The `direction` function sets the main axis, which dictates the direction items ## Align items and distribute extra space -The following sections describe how to align items and distribute extra space along the main and cross axes. +The following sections describe how to align items and distribute extra space +along the main and cross axes. ### Along the main axis -Use `justifyContent` to distribute items along the main axis. The following table shows the behavior when the direction is `Row`. +Use `justifyContent` to distribute items along the main axis. The following +table shows the behavior when the direction is `Row`. |---|---| | | ![Illustration of a horizontal main axis.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/main-axis.png) | @@ -49,7 +52,9 @@ Use `justifyContent` to distribute items along the main axis. The following tabl ### Along the cross axis -Use `alignItems` to align items along the cross axis within a single line. This behavior can be overridden by individual items using the [`alignSelf` modifier](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/item-behavior#item-alignment). +Use `alignItems` to align items along the cross axis within a single line. This +behavior can be overridden by individual items using the +[`alignSelf` modifier](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/item-behavior#item-alignment). The following images show the behavior when the direction is `Row`: @@ -57,7 +62,10 @@ The following images show the behavior when the direction is `Row`: | ![Illustration of a vertical cross axis.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/crossaxis.png) | ![Items aligned to the start of the cross axis.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/crossaxis-start.png) | ![Items aligned to the end of the cross axis.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/crossaxis-end.png) | ![Items aligned to the center of the cross axis.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/crossaxis-center.png) | ![Items stretched to fill the cross axis.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/crossaxis-stretch.png) | ![Items aligned to their baseline along the cross axis.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/crossaxis-baseline.png) | | | `Start` | `End` | `Center` | `Stretch` | `Baseline` | -Use `alignContent` to align lines to the cross axis and to distribute extra space between lines. This property only applies when there are multiple lines (wrapping is enabled). The following images show the behavior when the direction is `Row`: +Use `alignContent` to align lines to the cross axis and to distribute extra +space between lines. This property only applies when there are multiple lines +(wrapping is enabled). The following images show the behavior when the direction +is `Row`: |---|---|---|---|---|---|---| | ![Illustration of a vertical cross axis.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/crossaxis.png) | ![Multiple lines of items aligned to the start of the cross axis.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/aligncontent-start.png) | ![Multiple lines of items aligned to the end of the cross axis.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/aligncontent-end.png) | ![Multiple lines of items aligned to the center of the cross axis.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/aligncontent-center.png) | ![Multiple lines of items stretched to fill the cross axis.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/aligncontent-stretch.png) | ![Multiple lines of items distributed along the cross axis with space between them.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/aligncontent-spacebetween.png) | ![Multiple lines of items distributed along the cross axis with space around them.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/aligncontent-spacearound.png) | @@ -65,7 +73,9 @@ Use `alignContent` to align lines to the cross axis and to distribute extra spac ## Wrap items -Wrapping lets a `FlexBox` container become multi-line, moving items that don't fit onto a new row or column along the cross-axis. Configure wrapping behavior using `wrap`. +Wrapping lets a `FlexBox` container become multi-line, moving items that don't +fit onto a new row or column along the cross-axis. Configure wrapping behavior +using `wrap`. |---|---| | **`FlexWrap` value** | **Example using direction `Row`** | @@ -73,20 +83,29 @@ Wrapping lets a `FlexBox` container become multi-line, moving items that don't f | `Wrap`: When there is insufficient space for an item (plus any [gap](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/container-behavior#add-gaps)), a new line is created in the direction of the cross axis. For example, if the direction is `Row`, a new line is added **below**. | ![Items wrapping onto a new line below because wrapping is enabled.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/wrapitems-2.png) | | `WrapReverse`: The same as `Wrap`, except the new line is added in the opposite direction to the cross axis. For example, if the direction is `Row`, a new line is added **above**. | ![Items wrapping onto a new line above because reverse wrapping is enabled.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/wrapitems-3.png) | -The following example shows how the `FlexBox` wrapping algorithm works. The `FlexBox` container has a main size of `100dp`, with `wrap` set to `FlexWrap.Wrap` and a gap of `8dp`. It contains three items with `basis` `20dp`, `40dp`, and `50dp`, respectively. +The following example shows how the `FlexBox` wrapping algorithm works. The +`FlexBox` container has a main size of `100dp`, with `wrap` set to +`FlexWrap.Wrap` and a gap of `8dp`. It contains three items with `basis` `20dp`, +`40dp`, and `50dp`, respectively. -There is `100dp` available space in the line. Child 1 is `20dp`. There is space, so Child 1 is placed into the line. +There is `100dp` available space in the line. Child 1 is `20dp`. +There is space, so Child 1 is placed into the line. ![First item placed in the FlexBox container.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/algorithm-1.png) **Figure 1.** First item placed in the `FlexBox` container. -There is `80dp` available space in the line. The gap is `8dp`. Child 2 is `40dp`. The required space is `48dp`. There is space, so the gap and Child 2 are placed into the line. +There is `80dp` available space in the line. The gap is `8dp`. Child 2 is +`40dp`. The required space is `48dp`. There is space, so the gap and Child 2 +are placed into the line. ![Second item placed in the FlexBox container after the first item.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/algorithm-2.png) **Figure 2.** Second item placed in the `FlexBox` container after the first item. -There is `32dp` available space in the line. The gap is `8dp`. Child 3 is `50dp`. The required space is `58dp`. There is not enough space in the current line, so Child 3 is placed in a new line. +There is `32dp` available space in the line. The gap is `8dp`. Child 3 is +`50dp`. The required space is `58dp`. There is not enough space in the current +line, so Child 3 is placed in a new line. ![Third item placed on a new line because it doesn't fit on the first line.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/algorithm-3.png) **Figure 3.** Third item placed on a new line because it doesn't fit on the first line. ## Add gaps between items -Add gaps between rows and columns using `rowGap` and `columnGap`. This is useful to avoid adding spacing modifiers to children. +Add gaps between rows and columns using `rowGap` and `columnGap`. This is useful +to avoid adding spacing modifiers to children. |---|---|---| | ![Row gap adds vertical space between items.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/gap-1.png) | ![Column gap adds horizontal space between items.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/gap-2.png) | ![Gap adds both horizontal and vertical space between items.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/gap-3.png) | diff --git a/jetpack-compose/adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/get-started.md b/jetpack-compose/adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/get-started.md index 2bf41f0..cf860ca 100644 --- a/jetpack-compose/adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/get-started.md +++ b/jetpack-compose/adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/get-started.md @@ -2,10 +2,11 @@ This page describes how to implement basic `FlexBox` layouts. ## Set up project -1. Add the [`androidx.compose.foundation.layout`](https://developer.android.com/jetpack/androidx/versions) library to your project's `lib.versions.toml`. +1. Add the [`androidx.compose.foundation.layout`](https://developer.android.com/jetpack/androidx/versions) library to your project's + `lib.versions.toml`. [versions] - compose = "1.12.0-rc01" + compose = "1.13.0-alpha01" [libraries] androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "compose" } @@ -18,9 +19,9 @@ This page describes how to implement basic `FlexBox` layouts. ## Create basic FlexBox layouts -**Example 1** : `FlexBox` lays out two `Text` elements that are centrally aligned. +**Example 1** : `FlexBox` lays out two `Text` elements that are centrally +aligned. -
```kotlin FlexBox( @@ -32,16 +33,16 @@ FlexBox( Text(text = "Hello", fontSize = 48.sp) Text(text = "World!", fontSize = 48.sp) } - ```
![Hello World text composables stacked on top of each other in a basic FlexBox implementation.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/basic-flexbox.png) -**Example 2** : `FlexBox` wraps five items onto two rows and grows them unequally to fill the available space on each row. There is an `8.dp` gap, both vertically and horizontally, between the items. +**Example 2** : `FlexBox` wraps five items onto two rows and grows them unequally +to fill the available space on each row. There is an `8.dp` +gap, both vertically and horizontally, between the items. -
```kotlin FlexBox( @@ -58,11 +59,11 @@ FlexBox( OrangeRoundedBox(modifier = Modifier.flex { grow(1.0f) }) PinkRoundedBox(modifier = Modifier.flex { grow(1.0f) }) } - ```
![Two rows of colored items, with three unequally sized items distributed across the top row and two unequally sized items across the bottom row.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/basic-flexbox-2.png) -To learn more about `FlexBox` behavior, see [Set container behavior](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/container-behavior) and [Set item behavior](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/item-behavior). \ No newline at end of file +To learn more about `FlexBox` behavior, see [Set container behavior](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/container-behavior) and [Set +item behavior](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/item-behavior). \ No newline at end of file diff --git a/jetpack-compose/adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/index.md b/jetpack-compose/adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/index.md index 50828f0..ddda0de 100644 --- a/jetpack-compose/adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/index.md +++ b/jetpack-compose/adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/index.md @@ -1,7 +1,10 @@ > [!NOTE] > **Note:** FlexBox is an experimental API and is likely to change in the future. To use it, annotate your code with `@ExperimentalFlexBoxApi`. Please file any issues or feedback on the [issue tracker](https://issuetracker.google.com/issues/new?component=1876021&title=%5BFlexBox%5D). -[`FlexBox`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/FlexBox.composable#FlexBox(androidx.compose.ui.Modifier,androidx.compose.foundation.layout.FlexBoxConfig,kotlin.Function1)) is a container that lays out items in a single direction. It can resize, wrap, align, and distribute space among items to optimally fill the available space. It's a useful layout for different sized items and for resizing items when the available space changes. +[`FlexBox`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/FlexBox.composable#FlexBox(androidx.compose.ui.Modifier,androidx.compose.foundation.layout.FlexBoxConfig,kotlin.Function1)) is a container that lays out items in a single direction. It can +resize, wrap, align, and distribute space among items to optimally fill the +available space. It's a useful layout for different sized items and for resizing +items when the available space changes. With `FlexBox`, you can: @@ -11,16 +14,23 @@ With `FlexBox`, you can: ## When to use FlexBox -`FlexBox` is usually used to display a small number of items *within* an overall screen layout. For an overall screen layout, `Grid` is usually a better choice. `FlexBox` does not support lazy-loading of items. To display large numbers of items, use [lazy lists and grids](https://developer.android.com/develop/ui/compose/lists). If you need to wrap items, use `FlexBox` instead of `FlowRow` and `FlowColumn`. +`FlexBox` is usually used to display a small number of items *within* an +overall screen layout. For an overall screen layout, +`Grid` is usually a better choice. `FlexBox` does not support lazy-loading of +items. To display large numbers of items, use [lazy lists and grids](https://developer.android.com/develop/ui/compose/lists). If you +need to wrap items, use `FlexBox` instead of `FlowRow` and `FlowColumn`. ## Terminology and concepts > [!IMPORTANT] > **Key Point:** `FlexBox` is heavily influenced by the [CSS Flexible Box Layout specification](https://www.w3.org/TR/css-flexbox-1/) and has almost identical concepts, terminology, and behavior. If you're familiar with `display: flex`, you'll find `FlexBox`'s properties and behavior almost identical. -`FlexBox` lays out its items in either horizontal or vertical *lines* . This direction of these lines establishes the *main axis* . 90 degrees to the main axis is the *cross axis* . The length of the `FlexBox` along the main axis is known as the *main size* . The corresponding cross axis length is known as the *cross size* . These sizes and axes form the basis of `FlexBox`'s behavior. +`FlexBox` lays out its items in either horizontal or vertical *lines* . This +direction of these lines establishes the *main axis* . 90 degrees to the main +axis is the *cross axis* . The length of the `FlexBox` along the main axis is +known as the *main size* . The corresponding cross axis length is known as the +*cross size* . These sizes and axes form the basis of `FlexBox`'s behavior. -
![FlexBox with horizontal main axis and vertical cross axis.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/intro-row-2.png) **Figure 1.** Axes and sizes when the `FlexBox` direction is `Row`. ![FlexBox with vertical main axis and horizontal cross axis.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/intro-column.png) **Figure 2.** Axes and sizes when the `FlexBox` direction is `Column`. @@ -39,18 +49,33 @@ You can apply `FlexBox` properties in two ways: ### Understand the `FlexBox` layout algorithm -One of `FlexBox`'s most powerful features is its ability to resize its children to best fit the space available to it. Understanding how `FlexBox` does this can help you set `FlexBox` properties to optimize your UI for all possible sizes. +One of `FlexBox`'s most powerful features is its ability to resize its children +to best fit the space available to it. Understanding how `FlexBox` does this can +help you set `FlexBox` properties to optimize your UI for all possible sizes. `FlexBox`'s layout algorithm works in the following way: -1. **Calculate child base size** : Use the child's [`basis` value](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/item-behavior#set-initial-size) to calculate its initial size along the main axis before any extra space is distributed. +1. **Calculate child base size** : Use the child's [`basis` value](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/item-behavior#set-initial-size) + to calculate its initial size along the main axis before any extra space is + distributed. -2. **Sort the children** : Sort the children by their [`order`](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/item-behavior#item-order) values, if present. +2. **Sort the children** : Sort the children by their [`order`](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/item-behavior#item-order) values, if + present. -3. **Build lines** : For each child, check if its initial size plus [`gap`](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/container-behavior#add-gaps) will fit into the remaining space on the current line. If so, place this child into the line. If not, place it onto a new line if [wrapping is enabled](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/container-behavior#wrap-items), or place the item into the current line where it will overflow (it will be partially obscured by the edge of the container). +3. **Build lines** : For each child, check if its initial size plus + [`gap`](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/container-behavior#add-gaps) will fit into the remaining space on the current line. + If so, place this child into the line. If not, place it onto a new line if + [wrapping is enabled](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/container-behavior#wrap-items), or place the item into the current line + where it will overflow (it will be partially obscured by the edge of the + container). -4. **Align or resize items in the main axis** : For each line, distribute extra space *to* or between items by [resizing](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/item-behavior#item-size) or [aligning](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/container-behavior#main-axis) them. +4. **Align or resize items in the main axis** : For each line, distribute extra + space *to* or between items by [resizing](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/item-behavior#item-size) or + [aligning](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/container-behavior#main-axis) them. -5. **Align or resize items in the cross axis** : For each line, distribute extra space to or between items and lines by [stretching or aligning them](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/container-behavior#cross-axis). +5. **Align or resize items in the cross axis** : For each line, distribute extra + space to or between items and lines by [stretching or aligning + them](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/container-behavior#cross-axis). -Now that you're familiar with `FlexBox` concepts, see [Get started](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/get-started) to create a basic `FlexBox`. \ No newline at end of file +Now that you're familiar with `FlexBox` concepts, see [Get started](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/get-started) to +create a basic `FlexBox`. \ No newline at end of file diff --git a/jetpack-compose/adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/item-behavior.md b/jetpack-compose/adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/item-behavior.md index ddb0f02..e4a5c0f 100644 --- a/jetpack-compose/adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/item-behavior.md +++ b/jetpack-compose/adaptive/references/android/develop/ui/compose/layouts/adaptive/flexbox/item-behavior.md @@ -1,10 +1,10 @@ -Use `Modifier.flex` to control how an item changes size, order, and is aligned inside a `FlexBox`. +Use `Modifier.flex` to control how an item changes size, order, and is aligned +inside a `FlexBox`. ## Item size Use the `basis`, `grow`, and `shrink` functions to control an item's size. -
```kotlin FlexBox { @@ -16,14 +16,14 @@ FlexBox { } ) } - ```
### Set initial size -Use `basis` to specify the item's initial size before any extra space is distributed. You can think of this as the item's *preferred* size. +Use `basis` to specify the item's initial size before any extra space is +distributed. You can think of this as the item's *preferred* size. |---|---|---|---| | **Value type** | **Behavior** | **Code snippet** Note: The boxes have a maximum intrinsic size of `100dp` | **Example using container width `600dp`** | @@ -31,31 +31,48 @@ Use `basis` to specify the item's initial size before any extra space is distrib | Fixed `dp` | A fixed size in Dp. | ```kotlin FlexBox { RedRoundedBox( Modifier.flex { basis(200.dp) } ) BlueRoundedBox( Modifier.flex { basis(100.dp) } ) } ``` | ![Items sized to a fixed dp value using basis.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/initialsize-2.png) | | Percentage | A percentage of the container size. | ```kotlin FlexBox { RedRoundedBox( Modifier.flex { basis(0.7f) } ) BlueRoundedBox( Modifier.flex { basis(0.3f) } ) } ``` | ![Items sized as a percentage of container size using basis.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/initialsize-3.png) | -If the basis value is less than the item's intrinsic minimum size, the intrinsic minimum size is used instead. For example, if a `Text` item that contains a word requires `50dp` to display, but also has `basis = 10.dp`, a value of `50dp` is used. +If the basis value is less than the item's intrinsic minimum size, the intrinsic +minimum size is used instead. For example, if a `Text` item that contains a word +requires `50dp` to display, but also has `basis = 10.dp`, a +value of `50dp` is used. ### Grow items when there's space -Use `grow` to specify how much an item grows when there is extra space. This is space remaining in the `FlexBox` container after all the items' `basis` values have been added up. The `grow` value indicates *how much* of the extra space a given child will receive, relative to its siblings. By default, items won't grow. +Use `grow` to specify how much an item grows when there is extra space. This is +space remaining in the `FlexBox` container after all the items' `basis` values +have been added up. The `grow` value indicates *how much* of the extra space a +given child will receive, relative to its siblings. By default, items won't +grow. -The following example shows a `FlexBox` with three child items. Each has a basis value of `100dp`. The first child has a positive `grow` value. Since there is only one child with a `grow` value, the actual value is irrelevant - as long as it's positive, the child receives all the extra space. +The following example shows a `FlexBox` with three child items. Each has a basis +value of `100dp`. The first child has a positive `grow` value. Since there is +only one child with a `grow` value, the actual value is irrelevant - as long as +it's positive, the child receives all the extra space. The images show the `FlexBox` behavior when its container size is `600dp`. |---|---| | ```kotlin FlexBox { RedRoundedBox( title = "400dp", modifier = Modifier.flex { grow(1f) } ) BlueRoundedBox(title = "100dp") GreenRoundedBox(title = "100dp") } ``` | Each child has a basis value of `100dp`. There is `300dp` of extra space. ![Three items with 100dp basis each, in a 600dp container, before growth.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/growitems-1.png) Child 1 grows by `300dp` to fill the extra space. ![First item grows to fill 300dp of extra space.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/growitems-2.png) | -In the following example, the container size and `basis` size are the same. The difference is that each child has a different `grow` value. +In the following example, the container size and `basis` size are the same. The +difference is that each child has a different `grow` value. |---|---| | ```kotlin FlexBox { RedRoundedBox( title = "150dp", modifier = Modifier.flex { grow(1f) } ) BlueRoundedBox( title = "200dp", modifier = Modifier.flex { grow(2f) } ) GreenRoundedBox( title = "250dp", modifier = Modifier.flex { grow(3f) } ) } ``` | Each child has a basis value of `100dp`. There is `300dp` of extra space. ![Three items with 100dp basis each, in a 600dp container, before growth, with different grow values.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/growitems-3.png) The total grow value is 6. Child 1 grows by (1 / 6) \* 300 = `50dp` Child 2 grows by (2 / 6) \* 300 = `100dp` Child 3 grows by (3 / 6) \* 300 = `150dp` ![Items grow to fill 300dp of extra space based on relative grow values.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/flexbox/growitems-4.png) | ### Shrink items when there's insufficient space -Use `shrink` to specify how much an item shrinks when the `FlexBox` container has insufficient space for all the items. `shrink` works the same way as `grow` except that, instead of distributing *extra space* to items, the *space deficit* is distributed to items. The `shrink` value specifies how much of the space deficit the item receives, or rather, how much the item will shrink by. By default, items have a `shrink` value of `1f`, meaning they shrink equally. +Use `shrink` to specify how much an item shrinks when the `FlexBox` container +has insufficient space for all the items. `shrink` works the same way as `grow` +except that, instead of distributing *extra space* to items, the *space deficit* +is distributed to items. The `shrink` value specifies how much of the space +deficit the item receives, or rather, how much the item will shrink by. By +default, items have a `shrink` value of `1f`, meaning they shrink equally. -The following example shows two `Text` composables with the same text. The first child has a shrink value of `1f`, meaning it shrinks to absorb all the space deficit. +The following example shows two `Text` composables with the same text. The first +child has a shrink value of `1f`, meaning it shrinks to absorb all the space +deficit. -
```kotlin FlexBox { @@ -74,7 +91,6 @@ FlexBox { .flex { shrink(0f) } ) } - ```
@@ -89,11 +105,14 @@ As the container size shrinks, Child 1 shrinks. ## Item alignment -Use `alignSelf` to control how an item is aligned to the cross axis. This overrides the [`alignItems` property](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/container-behavior#align-distribute) of the container for this item. It has all the same possible values, with the addition of `Auto` which inherits the behavior of the `FlexBox` container. +Use `alignSelf` to control how an item is aligned to the cross axis. This +overrides the [`alignItems` property](https://developer.android.com/develop/ui/compose/layouts/adaptive/flexbox/container-behavior#align-distribute) of the container for this item. It +has all the same possible values, with the addition of `Auto` which inherits the +behavior of the `FlexBox` container. -For example, this `FlexBox` has `alignItems` set to `Start` and five children which override the cross axis alignment. +For example, this `FlexBox` has `alignItems` set to `Start` and five children +which override the cross axis alignment. -
```kotlin FlexBox( @@ -107,7 +126,6 @@ FlexBox( PinkRoundedBox(modifier = Modifier.flex { alignSelf(FlexAlignSelf.Stretch) }) OrangeRoundedBox(modifier = Modifier.flex { alignSelf(FlexAlignSelf.Baseline) }) } - ```
@@ -116,13 +134,19 @@ FlexBox( ## Item order -By default, `FlexBox` lays out items in the order that they are declared in code. Override this behavior using `order`. +By default, `FlexBox` lays out items in the order that they are declared in +code. Override this behavior using `order`. -The default value for `order` is zero, and `FlexBox` sorts items based on this value in ascending order. Any items that have the same `order` value are laid out in the same order they are declared in. Use negative and positive `order` values to move items to the start or end of a layout without changing where they are declared. +The default value for `order` is zero, and `FlexBox` sorts items based on this +value in ascending order. Any items that have the same `order` value are +laid out in the same order they are declared in. Use negative and positive +`order` values to move items to the start or end of a layout without changing +where they are declared. -The following example shows two child items. The first has the default `order` of zero, and the second has an order of `-1`. After sorting, Child 1 appears after Child 2. +The following example shows two child items. The first has the default `order` +of zero, and the second has an order of `-1`. After sorting, Child 1 appears +after Child 2. -
```kotlin FlexBox { @@ -139,7 +163,6 @@ FlexBox { } ) } - ```
diff --git a/jetpack-compose/adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/container-properties.md b/jetpack-compose/adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/container-properties.md index 28c8d13..fc4e3a5 100644 --- a/jetpack-compose/adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/container-properties.md +++ b/jetpack-compose/adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/container-properties.md @@ -1,4 +1,6 @@ -You can define a Grid container configuration to create flexible layouts that respond to different screen sizes and content types. This page describes how to do the following: +You can define a Grid container configuration to create flexible layouts +that respond to different screen sizes and content types. +This page describes how to do the following: - [Define a grid](https://developer.android.com/develop/ui/compose/layouts/adaptive/grid/container-properties#grid-definition): Set up the basic structure of rows and columns. - [Place items in a grid](https://developer.android.com/develop/ui/compose/layouts/adaptive/grid/container-properties#item-placement): Understand how items are placed into grid cells and how to change flow direction. @@ -7,9 +9,13 @@ You can define a Grid container configuration to create flexible layouts that re ## Define a grid -A grid consists of columns and rows. The [`Grid`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/Grid.composable#Grid(kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function1)) composable has a `config` parameter that accepts a lambda to define the columns and rows within [`GridConfigurationScope`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridConfigurationScope). The following example defines a grid that has three rows and two columns, each with a fixed size specified in [`Dp`](https://developer.android.com/reference/kotlin/androidx/compose/ui/unit/Dp): +A grid consists of columns and rows. +The [`Grid`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/Grid.composable#Grid(kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function1)) composable has a `config` parameter +that accepts a lambda to define the columns and rows +within [`GridConfigurationScope`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridConfigurationScope). +The following example defines a grid that has three rows and two columns, +each with a fixed size specified in [`Dp`](https://developer.android.com/reference/kotlin/androidx/compose/ui/unit/Dp): -
```kotlin Grid( @@ -23,18 +29,26 @@ Grid( } ) { } - ```
## Place items in a grid -`Grid` takes the UI elements in the `content` lambda and places them into grid cells. The grid lays out items regardless of whether you have explicitly defined the rows and columns. By default, `Grid` tries to place a UI element in the available grid cell in the row; if it can't, it places it in an available grid cell in the next row. If there are no empty cells, `Grid` creates a new row. +`Grid` takes the UI elements +in the `content` lambda and places them into grid cells. +The grid lays out items regardless of +whether you have explicitly defined the rows and columns. +By default, +`Grid` tries to place a UI element in the available grid cell in the row; +if it can't, it places it in an available grid cell in the next row. +If there are no empty cells, `Grid` creates a new row. -In the following example, the grid has six grid cells and places a card into each one (Figure 1). Each grid cell is `160dp` x `90dp`, making the total grid size `320dp` x `270dp`. +In the following example, the grid has six grid cells +and places a card into each one (Figure 1). +Each grid cell is `160dp` x `90dp`, +making the total grid size `320dp` x `270dp`. -
```kotlin Grid( @@ -54,16 +68,15 @@ Grid( Card5() Card6() } - ```
![Six cards are placed in a grid that has three rows and two columns.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/placement.png) **Figure 1**. Six cards are placed in a grid that has three rows and two columns. -To change this default behavior to filling by column, set the [`flow`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridConfigurationScope#flow()) property to [`GridFlow.Column`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridFlow#Column()). +To change this default behavior to filling by column, +set the [`flow`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridConfigurationScope#flow()) property to [`GridFlow.Column`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridFlow#Column()). -
```kotlin Grid( @@ -85,7 +98,6 @@ Grid( Card5() Card6() } - ```
@@ -94,16 +106,17 @@ Grid( ## Manage track sizing -Rows and columns are collectively referred to as a [grid track](https://developer.android.com/develop/ui/compose/layouts/adaptive/grid#grid-track). You can specify the size of a grid track using one of the following methods: +Rows and columns are collectively referred to as a [grid track](https://developer.android.com/develop/ui/compose/layouts/adaptive/grid#grid-track). +You can specify the size of a grid track using one of the following methods: - **Fixed** (`Dp`): Allocates a specific size (e.g., `column(180.dp)`). - **Percentage** (`Float`): Allocates a percentage of the total available space from `0.0f` to `1.0f` (e.g., `row(0.5f)` for 50%). - **Flexible** ([`Fr`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/Fr)): Distributes remaining space proportionally after fixed and percentage tracks are calculated. For example, if two rows are set to `1.fr` and `3.fr`, the latter receives 75% of the remaining height. -- **Intrinsic** : Sizes the track based on the content inside it. For more information, see [Determine grid track size intrinsically](https://developer.android.com/develop/ui/compose/layouts/adaptive/grid/container-properties#intrinsic-grid-track-size). +- **Intrinsic** : Sizes the track based on the content inside it. For more information, see [Determine grid track size intrinsically](https://developer.android.com/develop/ui/compose/layouts/adaptive/grid/container-properties#intrisic-grid-track-size). -The following example uses the different track sizing options to define the row heights: +The following example uses the different track sizing options +to define the row heights: -
```kotlin Grid( @@ -123,7 +136,6 @@ Grid( PastelYellowCard("Auto") } - ```
@@ -132,11 +144,14 @@ Grid( ### Set the minimum size for flexible grid tracks -When a grid container has no remaining space, a standard flexible track can shrink to `0.dp`. To prevent this and ensure content isn't crushed, use [`GridTrackSize.MinMax`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridTrackSize#MinMax(androidx.compose.ui.unit.Dp,androidx.compose.foundation.layout.Fr)) to enforce an explicit minimum size while keeping the track flexible. +When a grid container has no remaining space, +a standard flexible track can shrink to `0.dp`. +To prevent this and ensure content isn't crushed, +use [`GridTrackSize.MinMax`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridTrackSize#MinMax(androidx.compose.ui.unit.Dp,androidx.compose.foundation.layout.Fr)) +to enforce an explicit minimum size while keeping the track flexible. The following example allocates at least `100.dp` to the first row: -
```kotlin Grid( @@ -156,7 +171,6 @@ Grid( PastelGreenCard("Flex(1.fr)") PastelBlueCard("Fixed(200.dp)") } - ```
@@ -165,11 +179,18 @@ Grid( ### Set the minimum grid track size to place lazy lists -Standard flexible tracks automatically query the intrinsic sizes of their children to establish a base size. However, Jetpack Compose prohibits querying the intrinsic sizes of [`SubcomposeLayout`](https://developer.android.com/reference/kotlin/androidx/compose/ui/layout/SubcomposeLayout.composable#SubcomposeLayout(androidx.compose.ui.Modifier,kotlin.Function2)), which backs components, such as [`LazyColumn`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/lazy/LazyColumn.composable) and [`LazyRow`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/lazy/LazyRow.composable). +Standard flexible tracks automatically query the intrinsic sizes of +their children to establish a base size. +However, Jetpack Compose prohibits querying the intrinsic sizes of +[`SubcomposeLayout`](https://developer.android.com/reference/kotlin/androidx/compose/ui/layout/SubcomposeLayout.composable#SubcomposeLayout(androidx.compose.ui.Modifier,kotlin.Function2)), which backs components, +such as [`LazyColumn`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/lazy/LazyColumn.composable) and [`LazyRow`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/lazy/LazyRow.composable). -Placing a lazy list inside a standard flexible track causes an [`IllegalStateException`](https://developer.android.com/reference/java/lang/IllegalStateException) crash. To safely place lazy lists inside a flexible grid track, use `MinMax` with an explicit minimum size (such as `0.dp`) to bypass the intrinsic measurement pass. +Placing a lazy list inside a standard flexible track causes +an [`IllegalStateException`](https://developer.android.com/reference/java/lang/IllegalStateException) crash. +To safely place lazy lists inside a flexible grid track, +use `MinMax` with an explicit minimum size (such as `0.dp`) +to bypass the intrinsic measurement pass. -
```kotlin Grid( @@ -192,7 +213,6 @@ Grid( } } } - ```
@@ -201,15 +221,20 @@ Grid( ### Determine grid track size intrinsically -You can use [intrinsic sizing](https://developer.android.com/develop/ui/compose/layouts/intrinsic-measurements) for a `Grid` when you want the layout to adapt to the content, rather than forcing it into a fixed container. The grid track size is determined with the following values: +You can use [intrinsic sizing](https://developer.android.com/develop/ui/compose/layouts/intrinsic-measurements) for a `Grid` +when you want the layout to adapt to the content, +rather than forcing it into a fixed container. +The grid track size is determined with the following values: - [`GridTrackSize.MaxContent`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridTrackSize#MaxContent()): Use the content's maximum intrinsic size (e.g., the width is determined by the full length of the text in a text block with no wrapping). - [`GridTrackSize.MinContent`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridTrackSize#MinContent()): Use the content's minimum intrinsic size (e.g., the width is determined by the longest single word in a text block). - [`GridTrackSize.Auto`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridTrackSize#Auto()): Use a flexible size for a track that adapts based on available space. It behaves like `MaxContent` by default, but shrinks and wraps its content to fit within the parent container. -The following example places two texts side by side. The column size for the first text is determined by the required minimum width to display the text, and the second column width depends on the required maximum width of the text. +The following example places two texts side by side. +The column size for the first text is determined +by the required minimum width to display the text, +and the second column width depends on the required maximum width of the text. -
```kotlin Grid( @@ -223,7 +248,6 @@ Grid( Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras imperdiet.") Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras imperdiet.") } - ```
@@ -232,9 +256,13 @@ Grid( ## Set gaps between rows and columns -Once your grid tracks are sized, you can modify the [grid gap](https://developer.android.com/develop/ui/compose/layouts/adaptive/grid#grid-gap) to refine the spacing between the tracks. You can specify the column gap with the [`columnGap`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridConfigurationScope#columnGap(androidx.compose.ui.unit.Dp)) function, and the row gap with [`rowGap`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridConfigurationScope#rowGap(androidx.compose.ui.unit.Dp)). In the following example, there is a `16dp` gap between each row, and an `8dp` gap between each column (Figure 5). +Once your grid tracks are sized, +you can modify the [grid gap](https://developer.android.com/develop/ui/compose/layouts/adaptive/grid#grid-gap) to refine the spacing between the tracks. +You can specify the column gap with the [`columnGap`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridConfigurationScope#columnGap(androidx.compose.ui.unit.Dp)) function, +and the row gap with [`rowGap`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridConfigurationScope#rowGap(androidx.compose.ui.unit.Dp)). In the following example, +there is a `16dp` gap between each row, +and an `8dp` gap between each column (Figure 5). -
```kotlin Grid( @@ -256,16 +284,17 @@ Grid( Card5() Card6() } - ```
![Gaps between rows and columns.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/gaps.png) **Figure 6**. Gaps between rows and columns. -You can also use the convenience function [`gap`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridConfigurationScope#gap(androidx.compose.ui.unit.Dp)) to define gaps of the same column and row size, and to define column and gap sizes separately using a single function. The following code adds `8dp` gaps to the grid: +You can also use the convenience function [`gap`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridConfigurationScope#gap(androidx.compose.ui.unit.Dp)) +to define gaps of the same column and row size, +and to define column and gap sizes separately using a single function. +The following code adds `8dp` gaps to the grid: -
```kotlin Grid( @@ -286,7 +315,101 @@ Grid( Card5() Card6() } - ``` -
\ No newline at end of file +
+ +## Define grid areas with named areas + +Named areas allow you to attach names to groups of grid cells, +which are called [grid areas](https://developer.android.com/develop/ui/compose/layouts/adaptive/grid#grid-area). +You can use these names instead of [coordinate indexes](https://developer.android.com/develop/ui/compose/layouts/adaptive/grid/item-properties#position) +when placing UI elements in the grid. + +Using named areas has two main benefits to code readability: + +- When defining the grid layout, the purpose and placement of the expected content is clear. +- When adding the content, the purpose of that content is clear. + +To organize complex layouts clearly, you can decouple your physical grid +structure from child placement by defining semantic grid areas. + +Inside the `config` lambda, use the [`area`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridConfigurationScope#area(kotlin.Any,kotlin.Int,kotlin.Int,kotlin.Int,kotlin.Int)) function in +`GridConfigurationScope` to register named areas in the grid. +You can then assign child composables to these areas +using the [`gridItem`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridScope#(androidx.compose.ui.Modifier).gridItem(kotlin.Any,androidx.compose.ui.Alignment)) modifier +with the corresponding area identifier. The `area` function maps +a semantic identifier (such as an `enum` class value or a string key) to +a set of physical grid coordinates. Grid lines and indexes are **1-based** +(that is, the first row is `1`, and the first column is `1`). + +For example, you define a grid that has four area IDs: + + +```kotlin +/** + * An enum representing the IDs for named areas within the grid. + */ +enum class GridAreaNames { + Area1, + Area2, + Area3, + Area4 +} +``` + +
+ +Provide the name for the area using the `areaId` parameter along with the area's +cell coordinates and spans. +The `gridItem` modifier uses the `areaId` as a key +to assign each child item to its designated grid area, +as shown in the following example: + + +```kotlin +Grid( + config = { + // Define a single column that takes all available width. + repeat(2) { column(0.5f) } + + // Define four rows, each taking 25% of the total height. + repeat(4) { row(0.25f) } + + // Define named grid areas by associating an areaId with specific row and column indices. + // Row and column indices are 1-based. + area(areaId = GridAreaNames.Area1, row = 1, column = 1, columnSpan = 2) + area(areaId = GridAreaNames.Area2, row = 2, column = 1, rowSpan = 3) + area(areaId = GridAreaNames.Area3, rows = 2..3, columns = 2..2) + area(areaId = GridAreaNames.Area4, row = 4, column = 2) + + gap(4.dp) + }, + modifier = Modifier.size(360.dp) +) { + PastelRedCard( + "Area 1", + // Use Modifier.gridItem(areaId) to place this composable into the + // grid area defined with the matching ID in the config block. + modifier = Modifier.gridItem(areaId = GridAreaNames.Area1) + ) + PastelGreenCard( + "Area 2", + modifier = Modifier.gridItem(areaId = GridAreaNames.Area2) + ) + PastelBlueCard( + "Area 3", + modifier = Modifier.gridItem(areaId = GridAreaNames.Area3) + ) + PastelYellowCard( + "Area 4", + modifier = Modifier.gridItem(areaId = GridAreaNames.Area4) + ) +} +``` + +
+ +By using named areas, you can reorganize or adjust the physical layout grid (for +example, changing rows, columns, or track sizes) in the `config` lambda +without needing to modify the order or parameters of the child composables. \ No newline at end of file diff --git a/jetpack-compose/adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/get-started.md b/jetpack-compose/adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/get-started.md index 0a525c7..0d28f7b 100644 --- a/jetpack-compose/adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/get-started.md +++ b/jetpack-compose/adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/get-started.md @@ -2,10 +2,11 @@ This page describes how to implement basic [`Grid`](https://developer.android.co ## Set up project -1. Add the [`androidx.compose.foundation.layout`](https://developer.android.com/jetpack/androidx/versions) library to your project's `lib.versions.toml`. +1. Add the [`androidx.compose.foundation.layout`](https://developer.android.com/jetpack/androidx/versions) library to your project's + `lib.versions.toml`. [versions] - compose = "1.12.0-rc01" + compose = "1.13.0-alpha01" [libraries] androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "compose" } @@ -18,9 +19,9 @@ This page describes how to implement basic [`Grid`](https://developer.android.co ## Create a basic grid -The following example creates a basic 2x3 grid, with the columns and rows having a fixed size of `100.dp`. +The following example creates a basic 2x3 grid, +with the columns and rows having a fixed size of `100.dp`. -
```kotlin Grid( @@ -40,11 +41,11 @@ Grid( Card5(containerColor = PastelOrange) Card6(containerColor = PastelYellow) } - ```
![A basic grid consists of rows and columns with fixed size.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/six-cards-in-grid.png) **Figure 1**. A basic grid consists of rows and columns with fixed size. -To learn how to implement more advanced grids, see [Set container properties](https://developer.android.com/develop/ui/compose/layouts/adaptive/grid/container-properties) and [Set item properties](https://developer.android.com/develop/ui/compose/layouts/adaptive/grid/item-properties). \ No newline at end of file +To learn how to implement more advanced grids, +see [Set container properties](https://developer.android.com/develop/ui/compose/layouts/adaptive/grid/container-properties) and [Set item properties](https://developer.android.com/develop/ui/compose/layouts/adaptive/grid/item-properties). \ No newline at end of file diff --git a/jetpack-compose/adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/index.md b/jetpack-compose/adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/index.md index e48d9c9..316b046 100644 --- a/jetpack-compose/adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/index.md +++ b/jetpack-compose/adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/index.md @@ -1,16 +1,27 @@ > [!NOTE] > **Note:** `Grid` is an experimental API and is subject to change. File any issues on the [issue tracker](https://issuetracker.google.com/issues/new?component=1876021&template=1424126). -[`Grid`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/Grid.composable#Grid(kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function1)) is a Jetpack Compose API that lets you flexibly implement a two-dimensional layout. With this API, you can display items in multi-column or multi-row layouts that adapt to the available container size. +[`Grid`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/Grid.composable#Grid(kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function1)) is a Jetpack Compose API +that lets you flexibly implement a two-dimensional layout. +With this API, you can display items in multi-column +or multi-row layouts that adapt to the available container size. ![A flexible and adaptive two-dimensional layout with Grid](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/example.png) **Figure 1.** A flexible and adaptive two-dimensional layout with `Grid`. ## How is Grid different from similar composables? -Compose already offers similar components, such as [`LazyVerticalGrid`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/lazy/grid/LazyVerticalGrid.composable#LazyVerticalGrid(androidx.compose.foundation.lazy.grid.GridCells,androidx.compose.ui.Modifier,androidx.compose.foundation.lazy.grid.LazyGridState,androidx.compose.foundation.layout.PaddingValues,kotlin.Boolean,androidx.compose.foundation.layout.Arrangement.Vertical,androidx.compose.foundation.layout.Arrangement.Horizontal,androidx.compose.foundation.gestures.FlingBehavior,kotlin.Boolean,androidx.compose.foundation.OverscrollEffect,kotlin.Function1)). These components are mainly for visualization of large, homogeneous data sets--- for example, displaying a content catalog in a video streaming app. These components are NOT designed for the structural layout of a screen or complex component. +Compose already offers similar components, such as [`LazyVerticalGrid`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/lazy/grid/LazyVerticalGrid.composable#LazyVerticalGrid(androidx.compose.foundation.lazy.grid.GridCells,androidx.compose.ui.Modifier,androidx.compose.foundation.lazy.grid.LazyGridState,androidx.compose.foundation.layout.PaddingValues,kotlin.Boolean,androidx.compose.foundation.layout.Arrangement.Vertical,androidx.compose.foundation.layout.Arrangement.Horizontal,androidx.compose.foundation.gestures.FlingBehavior,kotlin.Boolean,androidx.compose.foundation.OverscrollEffect,kotlin.Function1)). +These components are mainly for visualization of large, homogeneous data sets--- +for example, displaying a content catalog in a video streaming app. +These components are NOT designed +for the structural layout of a screen or complex component. -You can also implement a two-dimensional layout by combining multiple `Row` and `Column` composables. However, this approach has some downsides, such as deep hierarchies and difficulties in adaptability. +You can also implement a two-dimensional layout +by combining multiple `Row` and `Column` composables. +However, this approach has some downsides, +such as deep hierarchies and difficulties in adaptability. -The following table provides an overview of which layouts are suitable for each API: +The following table provides an overview +of which layouts are suitable for each API: | Component | Purpose | |---|---| @@ -23,16 +34,24 @@ The following table provides an overview of which layouts are suitable for each ## Terminology -Familiarize yourself with the following terminology to understand how `Grid` works. +Familiarize yourself with the following terminology +to understand how `Grid` works. ### Grid line -A grid is made up of lines, which run horizontally and vertically. If your grid has three rows, it has four horizontal lines, including the one after the last row. In the following image, each dotted line represents a grid line: +A grid is made up of lines, which run horizontally and vertically. +If your grid has three rows, it has four horizontal lines, +including the one after the last row. +In the following image, each dotted line represents a grid line: ![The grid consists of four horizontal lines and three vertical lines.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/grid-line.png) **Figure 2**. The grid consists of four horizontal lines and three vertical lines. ### Grid track -A grid track is the space between two grid lines. A row track is between two horizontal lines, and a column track is between two vertical lines. To define the size of these tracks, assign a size to them when you create the grid. +A grid track is the space between two grid lines. +A row track is between two horizontal lines, +and a column track is between two vertical lines. +To define the size of these tracks, +assign a size to them when you create the grid. ![A grid track for the first row.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/grid-track.png) **Figure 3**. A grid track for the first row. ### Grid cell @@ -42,10 +61,13 @@ A grid cell is the intersection of a row and column track. ### Grid area -A grid area consists of several grid cells. You can define a grid area by making an item span multiple tracks. +A grid area consists of several grid cells. +You can define a grid area by making an item span multiple tracks. ![A grid area that consists of four grid cells.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/grid-area.png) **Figure 5**. A grid area that consists of four grid cells. ### Grid gap -A grid gap is the gutter between grid tracks. You can't place a UI element into a gap, but you can span a UI element across it. +A grid gap is the gutter between grid tracks. +You can't place a UI element into a gap, +but you can span a UI element across it. ![A grid gap between the first column and the second column.](https://developer.android.com/static/develop/ui/compose/images/layouts/adaptive/grid/grid-gap.png) **Figure 6**. A grid gap between the first column and the second column. \ No newline at end of file diff --git a/jetpack-compose/adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/item-properties.md b/jetpack-compose/adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/item-properties.md index 5c8787c..bf4205d 100644 --- a/jetpack-compose/adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/item-properties.md +++ b/jetpack-compose/adaptive/references/android/develop/ui/compose/layouts/adaptive/grid/item-properties.md @@ -1,18 +1,35 @@ -While the `Grid` config defines the overall structure, you use the [`gridItem`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridScope#(androidx.compose.ui.Modifier).gridItem(kotlin.Int,kotlin.Int,kotlin.Int,kotlin.Int,androidx.compose.ui.Alignment)) modifier to control the position, spanning, and alignment of items within that structure. +While the `Grid` config defines the overall structure, +you use the [`gridItem`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridScope#(androidx.compose.ui.Modifier).gridItem(kotlin.Int,kotlin.Int,kotlin.Int,kotlin.Int,androidx.compose.ui.Alignment)) modifier to control the position, spanning, +and alignment of items within that structure. ## Set the item position -Place an item into a specific track or cell with the `row` and `column` parameters. +Place an item into a specific track or cell +with the `row` and `column` parameters. +Coordinate indexes work best for straightforward grid layouts. +For larger or more complex layouts, define [named areas](https://developer.android.com/develop/ui/compose/layouts/adaptive/grid/container-properties#named-area) instead. -The `row` and `column` parameters specify the row and column track indexes that the item is placed in. Track indexes are 1-based---they start at one. Specifying only `row` or `column` (not both) places the item in the next available space in that track. Specifying both places the item into that cell. +The `row` and `column` parameters specify the row and column track indexes +that the item is placed in. +Track indexes are 1-based---they start at one. +Specifying only `row` or `column` (not both) places the item +in the next available space in that track. +Specifying both places the item into that cell. -Use a positive integer to specify the track index from the start. For example, to place an item in the first row and column, use `gridItem(row = 1, column = 1)`. +Use a positive integer to specify the track index from the start. +For example, to place an item in the first row and column, +use `gridItem(row = 1, column = 1)`. -Use a negative integer to specify the track relative to the end. For example, to place an item in the second-to-last row and column, use `gridItem(row = -2, column = -2)`. +Use a negative integer to specify the track relative to the end. +For example, to place an item in the second-to-last row and column, use +`gridItem(row = -2, column = -2)`. -In the following example, Card **#2** is placed in the second row and the second column. Card **#3** is assigned to the last row (indexed by -1), where it automatically occupies the first available column in that track (Figure 1). +In the following example, Card **#2** is placed +in the second row and the second column. +Card **#3** is assigned to the last row (indexed by -1), +where it automatically occupies +the first available column in that track (Figure 1). -
```kotlin Grid( @@ -30,7 +47,6 @@ Grid( Card2(modifier = Modifier.gridItem(row = 2, column = 2)) Card3(modifier = Modifier.gridItem(row = -1, column = -2)) } - ```
@@ -41,9 +57,16 @@ and Card #3 is placed in the first column in the third row.](https://developer.a ## Span rows and columns -Use the `rowSpan` and `columnSpan` parameters to span an item over multiple cells. You can place a UI element into a [grid area](https://developer.android.com/develop/ui/compose/layouts/adaptive/grid#grid-area), which is the area consisting of several [grid cells](https://developer.android.com/develop/ui/compose/layouts/adaptive/grid#grid-cell). The `gridItem` modifier lets you specify the grid area with the `rowSpan` and `columnSpan` parameters. In the following example, Card **#1** is placed in the area consisting of two rows and two columns (Figure 2). +Use the `rowSpan` and `columnSpan` parameters +to span an item over multiple cells. +You can place a UI element into a [grid area](https://developer.android.com/develop/ui/compose/layouts/adaptive/grid#grid-area), +which is the area consisting of several [grid cells](https://developer.android.com/develop/ui/compose/layouts/adaptive/grid#grid-cell). +The `gridItem` modifier lets you specify the grid area +with the `rowSpan` and `columnSpan` parameters. +In the following example, +Card **#1** is placed in the area consisting of two rows and two columns +(Figure 2). -
```kotlin Grid( @@ -63,7 +86,6 @@ Grid( Card3() Card4(modifier = Modifier.gridItem(columnSpan = 3)) } - ```
@@ -72,9 +94,11 @@ Grid( ## Set the alignment in a grid area -You can set the alignment of the UI element in a grid area by specifying it in the `alignment` parameter of the [`gridItem`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridScope#(androidx.compose.ui.Modifier).gridItem(kotlin.Int,kotlin.Int,kotlin.Int,kotlin.Int,androidx.compose.ui.Alignment)) modifier. In the following example, **#1** is placed in the center of the grid area consisting of two columns and two rows. +You can set the alignment of the UI element in a grid area +by specifying it in the `alignment` parameter of the [`gridItem`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/layout/GridScope#(androidx.compose.ui.Modifier).gridItem(kotlin.Int,kotlin.Int,kotlin.Int,kotlin.Int,androidx.compose.ui.Alignment)) modifier. +In the following example, **#1** is placed in the center of the grid area +consisting of two columns and two rows. -
```kotlin Grid( @@ -102,7 +126,6 @@ Grid( Card3() Card4(modifier = Modifier.gridItem(columnSpan = 3)) } - ```
@@ -112,9 +135,13 @@ consisting of two rows and two columns.](https://developer.android.com/static/de ## Auto-placement mixed with placed items -A UI element in `Grid` that has no position specification undergoes auto-placement. This example shows how you can mix auto-placed elements and the UI elements with specified grid cells. Card **#2** and Card **#4** are placed in specified grid cells, and the other items are auto-placed. +A UI element in `Grid` +that has no position specification undergoes auto-placement. +This example shows how you can mix auto-placed elements +and the UI elements with specified grid cells. +Card **#2** and Card **#4** are placed in specified grid cells, +and the other items are auto-placed. -
```kotlin Grid( @@ -136,7 +163,6 @@ Grid( Card5() Card6() } - ```
diff --git a/jetpack-compose/adaptive/references/android/develop/ui/compose/layouts/adaptive/mediaquery/index.md b/jetpack-compose/adaptive/references/android/develop/ui/compose/layouts/adaptive/mediaquery/index.md index f9fb3ed..37128b7 100644 --- a/jetpack-compose/adaptive/references/android/develop/ui/compose/layouts/adaptive/mediaquery/index.md +++ b/jetpack-compose/adaptive/references/android/develop/ui/compose/layouts/adaptive/mediaquery/index.md @@ -1,7 +1,10 @@ > [!NOTE] > **Note:** The `mediaQuery` function and the related data types are experimental and subject to change. File any issues on the [issue tracker](https://issuetracker.google.com/issues?q=componentid:1876021). -You need various types of information, such as device capability and app status, to update your app layout. Window width and height are the most commonly used information. In addition to that, you can refer to the following information: +You need various types of information, such as device capability +and app status, to update your app layout. +Window width and height are the most commonly used information. +In addition to that, you can refer to the following information: - Window posture - Pointing devices precision @@ -9,9 +12,17 @@ You need various types of information, such as device capability and app status, - Whether the camera and microphone are supported by the device - The distance between a user and the device display -Because the information is updated dynamically, you need to monitor it and trigger recomposition when any update happens. The [`mediaQuery`](https://developer.android.com/reference/kotlin/androidx/compose/ui/mediaQuery.composable#mediaQuery(kotlin.Function1)) function abstracts the details of the information retrieval and lets you focus on defining the condition to trigger the layout updates. The following example switches the layout to `TabletopLayout` when the foldable posture is tabletop: +Because the information is updated dynamically, +you need to monitor it and trigger recomposition when any update happens. +The [`mediaQuery`](https://developer.android.com/reference/kotlin/androidx/compose/ui/mediaQuery.composable#mediaQuery(kotlin.Function1)) function abstracts the details of the information retrieval +and lets you focus on defining the condition to trigger the layout updates. + +> [!NOTE] +> **Note:** "media" refers to the presentation medium that the UI is rendered on. The term "media" originates from [CSS media queries](https://www.w3.org/TR/mediaqueries-3/) which inspired the `mediaQuery` API. + +The following example switches the layout to `TabletopLayout` +when the foldable posture is tabletop: -
```kotlin @Composable @@ -26,16 +37,16 @@ fun VideoPlayer( } // ... } - ```
## Enable the `mediaQuery` function -To enable the `mediaQuery` function, set the `isMediaQueryIntegrationEnabled` attribute of the [`ComposeUiFlags`](https://developer.android.com/reference/kotlin/androidx/compose/ui/ComposeUiFlags) object to `true`: +To enable the `mediaQuery` function, +set the `isMediaQueryIntegrationEnabled` attribute of +the [`ComposeUiFlags`](https://developer.android.com/reference/kotlin/androidx/compose/ui/ComposeUiFlags) object to `true`: -
```kotlin class MyApplication : Application() { @@ -44,14 +55,20 @@ class MyApplication : Application() { super.onCreate() } } - ```
## Define a condition with parameters -You can define a condition as a lambda that is evaluated within [`UiMediaScope`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope). The `mediaQuery` function evaluates the condition according to the current status and the device capabilities. The function returns a boolean value, so you can determine the layout with conditional branches like an `if` expression. Table 1 describes the parameters available in `UiMediaScope`. +You can define a condition as a lambda +that is evaluated within [`UiMediaScope`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope). +The `mediaQuery` function evaluates the condition according to +the current status and the device capabilities. +The function returns a boolean value, +so you can determine the layout with conditional branches +like an `if` expression. +Table 1 describes the parameters available in `UiMediaScope`. | Parameter | Value type | Description | |---|---|---| @@ -64,9 +81,17 @@ You can define a condition as a lambda that is evaluated within [`UiMediaScope`] | `hasMicrophone` | `Boolean` | Whether the microphone is supported on the device. | | `viewingDistance` | [`UiMediaScope.ViewingDistance`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.ViewingDistance) | The typical distance between the user and the device screen. | -A `UiMediaScope` object resolves the values of the parameters. The `mediaQuery` function uses [`LocalUiMediaScope.current`](https://developer.android.com/reference/kotlin/androidx/compose/ui/package-summary#LocalUiMediaScope()) to access the `UiMediaScope` object, which represents the current device capabilities and context. This object is dynamically updated when any changes are made, such as when the user changes the device posture. The `mediaQuery` function then evaluates the `query` lambda with the updated `UiMediaScope` object and returns a boolean value. For example, the following snippet chooses between `TabletopLayout` and `FlatLayout` based on the `windowPosture` parameter value. +A `UiMediaScope` object resolves the values of the parameters. +The `mediaQuery` function uses [`LocalUiMediaScope.current`](https://developer.android.com/reference/kotlin/androidx/compose/ui/package-summary#LocalUiMediaScope()) +to access the `UiMediaScope` object, +which represents the current device capabilities and context. +This object is dynamically updated when any changes are made, +such as when the user changes the device posture. +The `mediaQuery` function then evaluates the `query` lambda +with the updated `UiMediaScope` object and returns a boolean value. +For example, the following snippet chooses between `TabletopLayout` +and `FlatLayout` based on the `windowPosture` parameter value. -
```kotlin @Composable @@ -81,18 +106,26 @@ fun VideoPlayer( } // ... } - ```
### Make a decision based on the window size -[Window size classes](https://developer.android.com/develop/ui/compose/layouts/adaptive/use-window-size-classes) are a set of opinionated viewport breakpoints that help you design, develop, and test adaptive layouts. You can compare the two parameters representing the current window size with the threshold defined in the window size classes. The following example changes the number of panes according to the window width. [`WindowSizeClass`](https://developer.android.com/reference/androidx/window/core/layout/WindowSizeClass) class has constants for the thresholds of window size classes (Figure 1). +[Window size classes](https://developer.android.com/develop/ui/compose/layouts/adaptive/use-window-size-classes) are a set of opinionated viewport breakpoints +that help you design, develop, and test adaptive layouts. +You can compare the two parameters representing the current window size +with the threshold defined in the window size classes. +The following example changes the number of panes according to the window width. +[`WindowSizeClass`](https://developer.android.com/reference/androidx/window/core/layout/WindowSizeClass) class has constants for the thresholds +of window size classes (Figure 1). -The [`derivedMediaQuery`](https://developer.android.com/reference/kotlin/androidx/compose/ui/derivedMediaQuery.composable#derivedMediaQuery(kotlin.Function1)) function evaluates the `query` lambda and wraps the result in a [`derivedStateOf`](https://developer.android.com/develop/ui/compose/side-effects#derivedstateof). Because `windowWidth` and `windowHeight` can update frequently, call the `derivedMediaQuery` function instead of the `mediaQuery` function when you refer to those parameters in the `query` lambda. +The [`derivedMediaQuery`](https://developer.android.com/reference/kotlin/androidx/compose/ui/derivedMediaQuery.composable#derivedMediaQuery(kotlin.Function1)) function evaluates the `query` lambda +and wraps the result in a [`derivedStateOf`](https://developer.android.com/develop/ui/compose/side-effects#derivedstateof). +Because `windowWidth` and `windowHeight` can update frequently, +call the `derivedMediaQuery` function instead of the `mediaQuery` function +when you refer to those parameters in the `query` lambda. -
```kotlin val narrowerThanMedium by derivedMediaQuery { @@ -106,7 +139,6 @@ when { narrowerThanExpanded -> TwoPaneLayout() else -> ThreePaneLayout() } - ```
@@ -115,9 +147,12 @@ when { ### Update layout according to the window posture -The `windowPosture` parameter describes the current window posture as a `UiMediaScope.Posture` object. You can check the current [posture](https://developer.android.com/develop/ui/compose/layouts/adaptive/foldables/learn-about-foldables) by comparing the parameter with the values defined in the `UiMediaScope.Posture` class. The following example switches layout according to the window posture: +The `windowPosture` parameter describes the current window posture +as a `UiMediaScope.Posture` object. +You can check the current [posture](https://developer.android.com/develop/ui/compose/layouts/adaptive/foldables/learn-about-foldables) by comparing the parameter +with the values defined in the `UiMediaScope.Posture` class. +The following example switches layout according to the window posture: -
```kotlin when { @@ -125,22 +160,32 @@ when { mediaQuery { windowPosture == UiMediaScope.Posture.Book } -> BookLayout() mediaQuery { windowPosture == UiMediaScope.Posture.Flat } -> FlatLayout() } - ```
### Check the precision of the available pointing device -A high precision pointing device helps users to point a UI element precisely. The precision of a pointing device depends on the device type. +A high precision pointing device helps users to point a UI element precisely. +The precision of a pointing device depends on the device type. -The `pointerPrecision` parameter describes the precision of the available pointing devices, such as a mouse and touchscreen. There are four values defined in the `UiMediaScope.PointerPrecision` class: [`Fine`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.PointerPrecision#Fine()), [`Coarse`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.PointerPrecision#Coarse()), [`Blunt`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.PointerPrecision#Blunt()), and [`None`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.PointerPrecision#None()). `None` means that no pointing device is available. The precision ranges from highest to lowest in this order: `Fine`, `Coarse`, and `Blunt`. +The `pointerPrecision` parameter describes the precision +of the available pointing devices, such as a mouse and touchscreen. +There are four values defined in the `UiMediaScope.PointerPrecision` class: +[`Fine`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.PointerPrecision#Fine()), [`Coarse`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.PointerPrecision#Coarse()), [`Blunt`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.PointerPrecision#Blunt()), and [`None`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.PointerPrecision#None()). +`None` means that no pointing device is available. +The precision ranges from highest to lowest in this order: +`Fine`, `Coarse`, and `Blunt`. -If multiple pointing devices are available and their precisions are different, the parameter is resolved with the highest one. For example, if there are two pointing devices --- a `Fine` precision device and a `Blunt` precision device --- `Fine` is the value of the `pointerPrecision` parameter. +If multiple pointing devices are available and their precisions are different, +the parameter is resolved with the highest one. +For example, if there are two pointing devices --- a `Fine` precision device and +a `Blunt` precision device --- +`Fine` is the value of the `pointerPrecision` parameter. -The following example shows a larger button when the user is using a pointing device with low precision: +The following example shows a larger button +when the user is using a pointing device with low precision: -
```kotlin if (mediaQuery { pointerPrecision == UiMediaScope.PointerPrecision.Blunt }) { @@ -148,31 +193,38 @@ if (mediaQuery { pointerPrecision == UiMediaScope.PointerPrecision.Blunt }) { } else { NormalSizeButton() } - ```
### Check the available keyboard type -The `keyboardKind` parameter represents the type of the available keyboards: [`Physical`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.KeyboardKind#Physical()), [`Virtual`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.KeyboardKind#Virtual()), and [`None`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.KeyboardKind#None()). If an on-screen keyboard is displayed and a hardware keyboard is available at the same time, the parameter is resolved as `Physical`. If neither is detected, `None` is the value of the parameter. The following example shows a message suggesting that users connect a keyboard when no keyboard is detected: +The `keyboardKind` parameter represents the type of the available keyboards: +[`Physical`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.KeyboardKind#Physical()), [`Virtual`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.KeyboardKind#Virtual()), and [`None`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.KeyboardKind#None()). +If an on-screen keyboard is displayed and +a hardware keyboard is available at the same time, +the parameter is resolved as `Physical`. +If neither is detected, `None` is the value of the parameter. +The following example shows a message suggesting that users connect a keyboard +when no keyboard is detected: -
```kotlin if (mediaQuery { keyboardKind == UiMediaScope.KeyboardKind.None }) { SuggestKeyboardConnect() } - ```
### Check if the device supports camera and microphone -Some devices don't support cameras or microphones. You can check if the device supports a camera and a microphone with the `hasCamera` parameter and the `hasMicrophone` parameter. The following example shows buttons to use with camera and microphone when the device supports them: +Some devices don't support cameras or microphones. +You can check if the device supports a camera and a microphone +with the `hasCamera` parameter and the `hasMicrophone` parameter. +The following example shows buttons to use with camera and microphone +when the device supports them: -
```kotlin Row { @@ -186,18 +238,25 @@ Row { CameraButton() } } - ```
### Adjust UI with the estimated viewing distance -Viewing distance is a factor that helps determine layout. If the user is using the app from a distance, they would expect the text and UI elements to be bigger. The `viewingDistance` parameter provides an estimate of the viewing distance based on the device type and its typical usage context. +Viewing distance is a factor that helps determine layout. +If the user is using the app from a distance, +they would expect the text and UI elements to be bigger. +The `viewingDistance` parameter provides an estimate of the viewing distance +based on the device type and its typical usage context. -There are three values defined in the `UiMediaScope.ViewingDistance` class: [`Near`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.ViewingDistance#Near()), [`Medium`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.ViewingDistance#Medium()), and [`Far`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.ViewingDistance#Far()). `Near` means that the screen is in close range, and `Far` means that the device is viewed from a distance. The following example increases the font size when the viewing distance is `Far` or `Medium`: +There are three values defined in the `UiMediaScope.ViewingDistance` class: +[`Near`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.ViewingDistance#Near()), [`Medium`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.ViewingDistance#Medium()), and [`Far`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.ViewingDistance#Far()). +`Near` means that the screen is in close range, +and `Far` means that the device is viewed from a distance. +The following example increases the font size when the viewing distance is +`Far` or `Medium`: -
```kotlin val fontSize = when { @@ -205,16 +264,19 @@ val fontSize = when { mediaQuery { viewingDistance == UiMediaScope.ViewingDistance.Medium } -> 18.sp else -> 16.sp } - ```
## Preview a UI component -You can call the `mediaQuery` and `derivedMediaQuery` functions in the composable functions to preview UI components. The following snippet chooses between `TabletopLayout` and `FlatLayout` based on the `windowPosture` parameter value. To preview the `TabletopLayout`, the `windowPosture` parameter should be [`UiMediaScope.Posture.Tabletop`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.Posture#Tabletop()). +You can call the `mediaQuery` and `derivedMediaQuery` functions in the +composable functions to preview UI components. +The following snippet chooses between `TabletopLayout` +and `FlatLayout` based on the `windowPosture` parameter value. +To preview the `TabletopLayout`, the `windowPosture` parameter should be +[`UiMediaScope.Posture.Tabletop`](https://developer.android.com/reference/kotlin/androidx/compose/ui/UiMediaScope.Posture#Tabletop()). -
```kotlin when { @@ -222,12 +284,14 @@ when { mediaQuery { windowPosture == UiMediaScope.Posture.Book } -> BookLayout() mediaQuery { windowPosture == UiMediaScope.Posture.Flat } -> FlatLayout() } - ```
-The `mediaQuery` and `derivedMediaQuery` functions evaluate the given `query` lambda within a `UiMediaScope` object, which is provided as `LocalUiMediaScope.current`. You can override it with the following steps: +The `mediaQuery` and `derivedMediaQuery` functions evaluate +the given `query` lambda within a `UiMediaScope` object, +which is provided as `LocalUiMediaScope.current`. +You can override it with the following steps: 1. Enable the `mediaQuery` function. 2. Define a custom object that implements the `UiMediaScope` interface. @@ -236,7 +300,6 @@ The `mediaQuery` and `derivedMediaQuery` functions evaluate the given `query` la You can preview the `TabletopLayout` with the following example: -
```kotlin @Preview @@ -265,7 +328,6 @@ fun PreviewLayoutForTabletop() { } } } - ```
\ No newline at end of file diff --git a/jetpack-compose/adaptive/references/android/develop/ui/compose/tooling/debug.md b/jetpack-compose/adaptive/references/android/develop/ui/compose/tooling/debug.md index 027181b..023836f 100644 --- a/jetpack-compose/adaptive/references/android/develop/ui/compose/tooling/debug.md +++ b/jetpack-compose/adaptive/references/android/develop/ui/compose/tooling/debug.md @@ -2,29 +2,60 @@ Tools for debugging your Compose UI are available in Android Studio. ## Layout Inspector -Layout Inspector lets you inspect a Compose layout inside a running app in an emulator or physical device. You can use the Layout Inspector to check how often a composable is recomposed or skipped, which can help identify issues with your app. For example, some coding errors might force your UI to recompose excessively, which can cause [poor performance](https://developer.android.com/develop/ui/compose/performance). Some coding errors can prevent your UI from recomposing and, therefore, prevent your UI changes from showing up on the screen. If you're new to Layout inspector, check the [guidance](https://developer.android.com/studio/debug/layout-inspector) on how to run it. +Layout Inspector lets you inspect a Compose layout inside a running app in an +emulator or physical device. You can use the Layout Inspector to check how often +a composable is recomposed or skipped, which can help identify issues with your +app. For example, some coding errors might force your UI to recompose +excessively, which can cause [poor performance](https://developer.android.com/develop/ui/compose/performance). +Some coding errors can prevent your UI from recomposing and, therefore, +prevent your UI changes from showing up on the screen. If you're new to +Layout inspector, check the [guidance](https://developer.android.com/studio/debug/layout-inspector) on how to +run it. > [!NOTE] > **Note:** If you're not seeing Compose components in layout inspector, make sure you are not removing `META-INF/androidx.compose.*.version` files from the APK. These are required for layout inspector to work. ### Get recomposition counts -When debugging your Compose layouts, knowing when composables [recompose](https://developer.android.com/develop/ui/compose/mental-model#recomposition) is important in understanding whether your UI is implemented properly. For example, if it's recomposing too many times, your app might be doing more work than is necessary. On the other hand, components that don't recompose when you anticipate them to can lead to unexpected behaviors. +When debugging your Compose layouts, knowing when composables +[recompose](https://developer.android.com/develop/ui/compose/mental-model#recomposition) is important in +understanding whether your UI is implemented properly. For example, if it's +recomposing too many times, your app might be doing more work than is necessary. +On the other hand, components that don't recompose when you anticipate them to +can lead to unexpected behaviors. -The Layout Inspector shows you when discrete composables in your layout hierarchy have either recomposed or skipped, as you interact with your app. In Android Studio, your recompositions are highlighted to help you determine where in the UI your composables are recomposing. +The Layout Inspector shows you when discrete composables in your layout +hierarchy have either recomposed or skipped, as you interact with your app. In +Android Studio, your recompositions are highlighted to help you determine +where in the UI your composables are recomposing. **Figure 1.** Recompositions are highlighted in Layout Inspector. -The highlighted portion shows a gradient overlay of the composable in the image section of the Layout Inspector, and gradually disappears so that you can get an idea of where in the UI the composable with the highest recompositions can be found. If one composable is recomposing at a higher rate than another composable, then the first composable receives a stronger gradient overlay color. If you double-click a composable in the layout inspector, you're taken to the corresponding code for analysis. +The highlighted portion shows a gradient overlay of the composable in the image +section of the Layout Inspector, and gradually disappears so that you can get an +idea of where in the UI the composable with the highest recompositions can be +found. If one composable is recomposing at a higher rate than another +composable, then the first composable receives a stronger gradient overlay +color. If you double-click a composable in the layout inspector, you're taken to +the corresponding code for analysis. > [!NOTE] > **Note:** To view recomposition counts, make sure your app is using an API level of 29 or higher, and `Compose 1.2.0` or higher. Then, deploy your app as you normally would. ![](https://developer.android.com/static/develop/ui/compose/images/li-recomposition-counts.png) **Figure 2.**The composition and skip counter in Layout Inspector. -Open the **Layout Inspector** window and connect to your app process. In the **Component Tree** , there are two columns that appear next to the layout hierarchy. The first column shows the number of compositions for each node and the second column displays the number of skips for each node. Selecting a composable node shows the dimensions and parameters of the composable, unless it's an inline function, in which case the parameters can't be shown. You can also see similar information in the **Attributes** pane when you select a composable from the **Component Tree** or the **Layout Display**. +Open the **Layout Inspector** window and connect to your app process. In the +**Component Tree** , there are two columns that appear next to the layout +hierarchy. The first column shows the number of compositions for each node and +the second column displays the number of skips for each node. Selecting a +composable node shows the dimensions and parameters of the composable, unless +it's an inline function, in which case the parameters can't be shown. You can +also see similar information in the **Attributes** pane when you select a +composable from the **Component Tree** or the **Layout Display**. -Resetting the count can help you understand recompositions or skips during a specific interaction with your app. If you want to reset the count, click **Reset** near the top of the **Component Tree** pane. +Resetting the count can help you understand recompositions or skips during a +specific interaction with your app. If you want to reset the count, click +**Reset** near the top of the **Component Tree** pane. > [!NOTE] > **Note:** If you don't see the new columns in the **Component Tree** pane, you can view them by selecting **Show Recomposition Counts** from the **View Options** menu ![Layout Inspector View Options @@ -37,24 +68,47 @@ Inspector](https://developer.android.com/static/develop/ui/compose/images/li-sho ### Compose semantics -In Compose, [Semantics](https://developer.android.com/develop/ui/compose/accessibility/semantics) describe your UI in an alternative manner that is understandable for [Accessibility](https://developer.android.com/develop/ui/compose/accessibility) services and for the [Testing](https://developer.android.com/develop/ui/compose/testing) framework. You can use the Layout Inspector to inspect semantic information in your Compose layouts. +In Compose, [Semantics](https://developer.android.com/develop/ui/compose/accessibility/semantics) describe your UI in an +alternative manner that is understandable for +[Accessibility](https://developer.android.com/develop/ui/compose/accessibility) services and for the +[Testing](https://developer.android.com/develop/ui/compose/testing) framework. You can use the Layout Inspector +to inspect semantic information in your Compose layouts. ![Semantic information displayed using the Layout Inspector.](https://developer.android.com/static/develop/ui/compose/images/layout_inspector_semantics_new.png) **Figure 4.** Semantic information displayed using the Layout Inspector. -When selecting a Compose node, use the **Attributes** pane to check whether it declares semantic information directly, merges semantics from its children, or both. To quickly identify which nodes include semantics, either declared or merged, use select the **View options** drop-down in the **Component Tree** pane and select **Highlight Semantics Layers**. This highlights only the nodes in the tree that include semantics, and you can use your keyboard to quickly navigate between them. +When selecting a Compose node, use the **Attributes** pane to check whether it +declares semantic information directly, merges semantics from its children, or +both. To quickly identify which nodes include semantics, either declared or +merged, use select the **View options** drop-down in the **Component Tree** pane +and select **Highlight Semantics Layers**. This highlights only the nodes in the +tree that include semantics, and you can use your keyboard to quickly navigate +between them. ## Compose UI Check -To help you build more adaptive and accessible UIs in Jetpack Compose, Android Studio provides a UI Check mode in Compose Preview. This feature is similar to [Accessibility Scanner](https://developer.android.com/guide/topics/ui/accessibility/testing#accessibility-scanner) for views. +To help you build more adaptive and accessible UIs in Jetpack Compose, Android +Studio provides a UI Check mode in Compose Preview. This feature is similar +to [Accessibility Scanner](https://developer.android.com/guide/topics/ui/accessibility/testing#accessibility-scanner) +for views. -When you activate Compose UI check mode on a Compose Preview, Android Studio automatically audits your Compose UI and suggests improvements to make your UI more accessible and adaptive. Android Studio checks that your UI works across different screen sizes. In the **Problems** panel, the tool shows the issues that it detects, such as text stretched on large screens or low color contrast. +When you activate Compose UI check mode on a Compose Preview, Android Studio +automatically audits your Compose UI and suggests improvements to make your UI +more accessible and adaptive. Android Studio checks that your UI works across +different screen sizes. In the **Problems** panel, the tool shows the issues +that it detects, such as text stretched on large screens or low color contrast. To access this feature, click the UI Check icon on Compose Preview: ![](https://developer.android.com/static/studio/images/design/compose-ui-check-entry.png) **Figure 5.** Entry point to UI check mode. -UI check automatically previews your UI in different configurations and highlights issues found in different configurations. In the **Problems** panel, when you click an issue, you can see the details of the issue, suggested fixes, and the renderings that highlight the area of the issue. +UI check automatically previews your UI in different configurations and +highlights issues found in different configurations. In the **Problems** panel, +when you click an issue, you can see the details of the issue, suggested fixes, +and the renderings that highlight the area of the issue. ![](https://developer.android.com/static/studio/images/design/compose-ui-check.png) **Figure 6.** UI check mode in action. ### Fix with AI -For issues detected in UI Check mode, you can use the AI agent to propose and apply code fixes. Click the **Fix with AI** button on an issue in the **Problems** panel. The agent analyzes the problem and your code to suggest changes that resolve the accessibility or adaptive issue. +For issues detected in UI Check mode, you can use the AI agent to propose and +apply code fixes. Click the **Fix with AI** button on an issue in the +**Problems** panel. The agent analyzes the problem and your code to suggest +changes that resolve the accessibility or adaptive issue. ![](https://developer.android.com/static/studio/preview/features/images/ui-check-mode-single-fix.png) **Figure 7.** The agent fixes UI issues in UI Check mode. \ No newline at end of file diff --git a/jetpack-compose/adaptive/references/android/guide/navigation/navigation-3/recipes/material-listdetail.md b/jetpack-compose/adaptive/references/android/guide/navigation/navigation-3/recipes/material-listdetail.md index 032a997..fbaae79 100644 --- a/jetpack-compose/adaptive/references/android/guide/navigation/navigation-3/recipes/material-listdetail.md +++ b/jetpack-compose/adaptive/references/android/guide/navigation/navigation-3/recipes/material-listdetail.md @@ -138,6 +138,4 @@ class MaterialListDetailActivity : ComponentActivity() { } } } - - ``` \ No newline at end of file diff --git a/jetpack-compose/migration/migrate-xml-views-to-jetpack-compose/SKILL.md b/jetpack-compose/migration/migrate-xml-views-to-jetpack-compose/SKILL.md index c51cc9d..0c05ab1 100644 --- a/jetpack-compose/migration/migrate-xml-views-to-jetpack-compose/SKILL.md +++ b/jetpack-compose/migration/migrate-xml-views-to-jetpack-compose/SKILL.md @@ -9,7 +9,7 @@ description: Provides a structured workflow for migrating an Android XML View to license: Complete terms in LICENSE.txt metadata: author: Google LLC - last-updated: '2026-08-06' + last-updated: '2026-08-14' keywords: - Jetpack Compose - migration @@ -20,11 +20,16 @@ metadata: - UI development --- -This skill guides through the process of migrating an existing Android XML View to Jetpack Compose. It performs a stable, safe and visually consistent transition by following a structured, 10-step methodology. This skill migrates UI (XML to Jetpack Compose) only. +This skill guides through the process of migrating an existing Android XML View +to Jetpack Compose. It performs a stable, safe and visually consistent +transition by following a structured, 10-step methodology. This skill migrates +UI (XML to Jetpack Compose) only. ## Objective -To systematically convert a single legacy XML layout into modern, declarative Jetpack Compose UI while maintaining pixel-perfect visual parity and functional integrity. +To systematically convert a single legacy XML layout into modern, declarative +Jetpack Compose UI while maintaining pixel-perfect visual parity and functional +integrity. ## Summary of the 10-step migration process @@ -43,31 +48,61 @@ To systematically convert a single legacy XML layout into modern, declarative Je ### Step 1: Identify the optimal XML candidate for migration -If the user has explicitly specified a target XML layout, proceed to Step 2. Otherwise, analyze the codebase to identify the best candidate for migration by following the logic in [references/identify-optimal-xml-candidate.md](references/identify-optimal-xml-candidate.md). +If the user has explicitly specified a target XML layout, proceed to Step 2. +Otherwise, analyze the codebase to identify the best candidate for migration by +following the logic in [references/identify-optimal-xml-candidate.md](references/identify-optimal-xml-candidate.md). ### Step 2: Analyze the project and layout -Analyze the identified XML View's structure, hierarchy, and implementation details. Use [references/analysis-of-the-project-and-layout.md](references/analysis-of-the-project-and-layout.md) to guide your technical audit of the layout and surrounding project context. +Analyze the identified XML View's structure, hierarchy, and implementation +details. +Use [references/analysis-of-the-project-and-layout.md](references/analysis-of-the-project-and-layout.md) to +guide your technical audit of the layout and surrounding project context. ### Step 3: Create a plan -Using the outputs and analysis done in the Step 1 and 2, generate a step-by-step plan for the migration. If you support user interaction, present to the user and ask for approval before proceeding. If user interaction is not supported, proceed to Step 4 following the generated plan. +Using the outputs and analysis done in the Step 1 and 2, generate a +step-by-step plan for the migration. If you support user interaction, present +to the user and ask for approval before proceeding. If user interaction is not +supported, proceed to Step 4 following the generated plan. ### Step 4: Capture the XML View UI -**IF** you support user interaction, ask the user to upload a screenshot of the XML View UI or provide an absolute path to a file. Use this image as a visual reference for the layout migration in Step 7. **ELSE IF** you are able to run an Android emulator, locate an existing screenshot test for the XML candidate. If none exists, create one using the existing project testing framework. If no framework exists, use **UI Automator** or **Espresso** to create a screenshot test with minimum required setup. Run the test and take a baseline screenshot of the XML UI. **ELSE** proceed to Step 5. +**IF** you support user interaction, ask the user to upload a screenshot of the +XML View UI or provide an absolute path to a file. Use this image as a visual +reference for the layout migration in Step 7. +**ELSE IF** you are able to run an Android emulator, locate an existing +screenshot test for the XML candidate. If none exists, create one using the +existing project testing framework. If no framework exists, +use **UI Automator** or **Espresso** to create a screenshot test with minimum +required setup. Run the test and take a baseline screenshot of the XML UI. +**ELSE** proceed to Step 5. ### Step 5: Set up Compose dependencies and compiler -Check `build.gradle` or `libs.versions.toml` for Compose dependencies and compiler setup. If missing, use [Setup Compose Dependencies and Compiler](references/android/develop/ui/compose/setup-compose-dependencies-and-compiler.md). Run a sync to ensure dependencies resolve without errors. +Check `build.gradle` or `libs.versions.toml` for Compose dependencies and +compiler setup. If missing, use +[Setup Compose Dependencies and Compiler](references/android/develop/ui/compose/setup-compose-dependencies-and-compiler.md). +Run a sync to ensure dependencies resolve without errors. ### Step 6: Set up Compose theming -If the project already has Compose theming set up, proceed to Step 7. If Compose theming is missing, initialize it. For Material-based projects, follow [Material 3 migration guidelines](references/android/develop/ui/compose/designsystems/migrate-xml-theme-to-compose.md). For custom design systems, apply expert judgment to migrate XML theming and match existing styles. **Constraints:** Do not migrate the entire theme. Implement only the minimum theming required for the specific XML candidate. Maintain original XML themes for interoperability. Maintain existing project code conventions, patterns, names and values. +If the project already has Compose theming set up, proceed to Step 7. If Compose +theming is missing, initialize it. For Material-based projects, follow +[Material 3 migration guidelines](references/android/develop/ui/compose/designsystems/migrate-xml-theme-to-compose.md). +For custom design systems, apply expert judgment to migrate XML theming and +match existing styles. +**Constraints:** Do not migrate the entire theme. Implement only the minimum +theming required for the specific XML candidate. Maintain original XML themes +for interoperability. Maintain existing project code conventions, patterns, +names and values. ### Step 7: Migrate the XML View to Compose -Convert the XML candidate to Jetpack Compose code, referencing [references/xml-layout-migration.md](references/xml-layout-migration.md) and the image from Step 4. You must include a **Compose Preview** for the newly created composable to facilitate visual verification. +Convert the XML candidate to Jetpack Compose code, referencing +[references/xml-layout-migration.md](references/xml-layout-migration.md) and the image from Step 4. +You must include a **Compose Preview** for the newly created composable to +facilitate visual verification. ### Step 8: Replace usages @@ -78,8 +113,12 @@ Replace the usages of the migrated XML layout to use the new Compose component. ### Step 9: Validate the migration -Compare the baseline screenshot image from Step 4 with the rendered Compose Preview of the new composable. Ignore string content; focus on layout and styling. Iterate on the Compose code until visual parity is achieved. Once verified, write a Compose UI test for the new composable. +Compare the baseline screenshot image from Step 4 with the rendered Compose +Preview of the new composable. Ignore string content; focus on layout and +styling. Iterate on the Compose code until visual parity is achieved. Once +verified, write a Compose UI test for the new composable. ### Step 10: XML code removal -Delete the migrated XML file and its associated legacy tests. **Caution:** Only remove code and resources that are not referenced by other parts of the project. +Delete the migrated XML file and its associated legacy tests. **Caution:** Only +remove code and resources that are not referenced by other parts of the project. diff --git a/jetpack-compose/migration/migrate-xml-views-to-jetpack-compose/references/analysis-of-the-project-and-layout.md b/jetpack-compose/migration/migrate-xml-views-to-jetpack-compose/references/analysis-of-the-project-and-layout.md index 8b34a75..3e2f0c3 100644 --- a/jetpack-compose/migration/migrate-xml-views-to-jetpack-compose/references/analysis-of-the-project-and-layout.md +++ b/jetpack-compose/migration/migrate-xml-views-to-jetpack-compose/references/analysis-of-the-project-and-layout.md @@ -1,22 +1,41 @@ ## 1. Project health \& build validation -Before performing any analysis, you must confirm the project is in a functional state. \* **Integrity check:** Verify the project syncs (Gradle) and builds successfully. \* **Error resolution:** If there are pre-existing build errors or sync failures, you must report these immediately and attempt to fix. **Do not proceed** with migration until a stable baseline is established. +Before performing any analysis, you must confirm the project is in a functional state. +\* **Integrity check:** Verify the project syncs (Gradle) and builds successfully. +\* **Error resolution:** If there are pre-existing build errors or sync failures, you must report these immediately and attempt to fix. **Do not proceed** with migration until a stable baseline is established. ## 2. Compose pattern \& consistency analysis -If Jetpack Compose is already present, you must align with the established implementation style. \* **Pattern identification:** Scan the codebase for `@Composable` functions. Identify the project's "Best Practices" regarding state hoisting, composable construction and naming conventions, and file organization. \* **Theming review:** Determine how `MaterialTheme` or custom theme systems are implemented. \* Identify if the project uses a custom design system theme. \* Map how attributes, styles, and other theme components are accessed in Compose. +If Jetpack Compose is already present, you must align with the established implementation style. +\* **Pattern identification:** Scan the codebase for `@Composable` functions. Identify the project's "Best Practices" regarding state hoisting, composable construction and naming conventions, and file organization. +\* **Theming review:** Determine how `MaterialTheme` or custom theme systems are implemented. +\* Identify if the project uses a custom design system theme. +\* Map how attributes, styles, and other theme components are accessed in Compose. ## 3. Design system \& infrastructure audit -Understand the design system classification (e.g. Material 2, Material 3, or custom design system). \* **Resource mapping:** Locate central XML definitions: \* `colors.xml` (Light/Dark variants) \* `dimens.xml` \* `styles.xml` / `themes.xml` \* **Hybrid analysis:** Determine if the project is **XML-only** , **Compose-only** , or **Hybrid** . \* **Reuse constraint:** If a Compose theming layer (e.g., `AppTheme.kt`) already exists, **DO NOT** generate a new one. You must reuse the existing infrastructure and contribute to it by following its existing implementation pattern. +Understand the design system classification (e.g. Material 2, Material 3, or custom design system). +\* **Resource mapping:** Locate central XML definitions: +\* `colors.xml` (Light/Dark variants) +\* `dimens.xml` +\* `styles.xml` / `themes.xml` +\* **Hybrid analysis:** Determine if the project is **XML-only** , **Compose-only** , or **Hybrid** . +\* **Reuse constraint:** If a Compose theming layer (e.g., `AppTheme.kt`) already exists, **DO NOT** generate a new one. You must reuse the existing infrastructure and contribute to it by following its existing implementation pattern. ## 4. Candidate layout decomposition -Analyze the specific XML layout targeted for migration. You must extract and document the following requirements for the new composable: \* **Inputs:** UI State objects, primitive parameters, and click listeners. \* **Styling:** Specific color constants, typography styles, and shape definitions referenced in the XML. \* **Resources:** Identifying string resources, drawables, and dimensions. \* **Layout logic:** Modifiers required to replicate the XML constraints (padding, alignment, weight). +Analyze the specific XML layout targeted for migration. You must extract and document the following requirements for the new composable: +\* **Inputs:** UI State objects, primitive parameters, and click listeners. +\* **Styling:** Specific color constants, typography styles, and shape definitions referenced in the XML. +\* **Resources:** Identifying string resources, drawables, and dimensions. +\* **Layout logic:** Modifiers required to replicate the XML constraints (padding, alignment, weight). ## 5. Architectural \& non-UI analysis -Understand the environment in which the UI resides to ensure proper integration. \* **State management:** Identify the usage of `ViewModel`, `Flow`, or `LiveData`. \* **Dependency Injection:** Check for Hilt, Koin, or manual DI to understand how dependencies are provided to the UI layer. \* **Testing \& architecture:** Note the architectural pattern (MVI, MVVM, or custom architecture setup.) and existing UI testing frameworks to ensure the migrated code remains testable. Unless the user explicitly requests, **DO NOT** make any changes to any non-UI code that aren't strictly required for the migration of the XML View. +Understand the environment in which the UI resides to ensure proper integration. +\* **State management:** Identify the usage of `ViewModel`, `Flow`, or `LiveData`. +\* **Dependency Injection:** Check for Hilt, Koin, or manual DI to understand how dependencies are provided to the UI layer. +\* **Testing \& architecture:** Note the architectural pattern (MVI, MVVM, or custom architecture setup.) and existing UI testing frameworks to ensure the migrated code remains testable. Unless the user explicitly requests, **DO NOT** make any changes to any non-UI code that aren't strictly required for the migration of the XML View. *** ** * ** *** diff --git a/jetpack-compose/migration/migrate-xml-views-to-jetpack-compose/references/android/develop/ui/compose/designsystems/migrate-xml-theme-to-compose.md b/jetpack-compose/migration/migrate-xml-views-to-jetpack-compose/references/android/develop/ui/compose/designsystems/migrate-xml-theme-to-compose.md index 3eb0b20..60b7968 100644 --- a/jetpack-compose/migration/migrate-xml-views-to-jetpack-compose/references/android/develop/ui/compose/designsystems/migrate-xml-theme-to-compose.md +++ b/jetpack-compose/migration/migrate-xml-views-to-jetpack-compose/references/android/develop/ui/compose/designsystems/migrate-xml-theme-to-compose.md @@ -1,8 +1,14 @@ -When you introduce Compose in an existing app, you need to migrate your Material XML themes to use `MaterialTheme` for Compose components. This means your app's theming will have two sources of truth: the View-based theme and the Compose theme. Any changes to your styling need to be made in multiple places. Once your app is fully migrated to Compose, remove your XML theming. +When you introduce Compose in an existing app, you need to migrate your Material +XML themes to use `MaterialTheme` for Compose components. This means your app's +theming will have two sources of truth: the View-based theme and the Compose +theme. Any changes to your styling need to be made in multiple places. Once +your app is fully migrated to Compose, remove your XML theming. -You can use the [Material Theme Builder](https://m3.material.io/theme-builder) tool for migrating colors. +You can use the [Material Theme Builder](https://m3.material.io/theme-builder) +tool for migrating colors. -When you start the migration from XML to Compose, migrate the theming to Material 3 Compose theming. +When you start the migration from XML to Compose, migrate the theming to +Material 3 Compose theming. ## Glossary @@ -23,19 +29,26 @@ Before migrating, be aware of the following limitations: ## Step 1: Evaluate the design system -Identify which design system is used in the XML View project. Analyze the migration path and necessary steps to migrate the existing design system to Material 3 in Compose. +Identify which design system is used in the XML View project. +Analyze the migration path and necessary steps to migrate the existing design +system to Material 3 in Compose. ## Step 2: Identify theme source files -In XML you write `?attr/colorPrimary`. In Compose, you access theme values with `MaterialTheme.*`: +In XML you write `?attr/colorPrimary`. In Compose, you access theme values +with `MaterialTheme.*`: -Identify and locate all XML resources and files necessary for theming: light and dark color schemes and qualifiers, themes, shapes, dimensions, typography, styles and other relevant files. +Identify and locate all XML resources and files necessary for theming: +light and dark color schemes and qualifiers, themes, shapes, dimensions, +typography, styles and other relevant files. Resources such as strings can be reused as is and don't need to be migrated. ## Step 3: Migrate colors -**Key principle:** XML uses named hex colors. Material 3 uses *semantic roles* (e.g., `primary`, `onPrimary`, `surface`). Stop naming colors by their hex; name them by their role. +**Key principle:** XML uses named hex colors. +Material 3 uses *semantic roles* (e.g., `primary`, `onPrimary`, `surface`). +Stop naming colors by their hex; name them by their role. Examples: @@ -56,7 +69,8 @@ Examples: *** ** * ** *** -Migrate the dark and light color schemes from XML to their equivalents in Material 3 Compose. +Migrate the dark and light color schemes from XML to their equivalents in +Material 3 Compose. > [!NOTE] > **Note:** Material 3 naming differs from Material 2 color naming. @@ -103,14 +117,20 @@ XML styles (styles.xml) system defines styles and appearance of: 3. Themes and overlays 4. Shapes -XML Views and components combine multiple attributes to create a style. They set their styles from styles.xml in two different ways: +XML Views and components combine multiple attributes to create a style. +They set their styles from styles.xml in two different ways: 1. Setting "style="@style/..." directly and explicitly in the XML View 2. Setting the style indirectly and implicitly for a component as part of a larger Theme (theme.xml) -Styles have no **direct** equivalent in Compose - instead styles are passed as: parameters or modifiers to composables, using the [new, experimental Styles API](https://developer.android.com/develop/ui/compose/styles) defined in the AppTheme, or by creating layered, reusable composable variations with the defined style. +Styles have no **direct** equivalent in Compose - instead styles are passed as: +parameters or modifiers to composables, using the +[new, experimental Styles API](https://developer.android.com/develop/ui/compose/styles) defined in the AppTheme, or by creating +layered, reusable composable variations with the defined style. -Provide separate @Composable functions named according to the style and the base component, to signify the difference in styling and use cases for those components. +Provide separate @Composable functions named according to the style and the +base component, to signify the difference in styling and use cases for those +components. - **Pattern:** If an XML element uses a custom style (e.g., `style="@style/MyPrimaryButton"`), don't try to replicate the style inline. Instead, suggest creating a specific composable. - **Example:** @@ -142,6 +162,10 @@ Provide separate @Composable functions named according to the style and the base ## Step 6: Validate the theme migration -Always use the existing theme values from the original XML theme as the source of truth for the new Material Theme in Compose. Never invent new theme values during migration, to maintain brand consistency and avoid visual regressions. +Always use the existing theme values from the original XML theme as the source +of truth for the new Material Theme in Compose. +Never invent new theme values during migration, to maintain brand consistency +and avoid visual regressions. -Verify all new Compose theme values match the existing XML values. Don't hardcode any migrated values. \ No newline at end of file +Verify all new Compose theme values match the existing XML values. +Don't hardcode any migrated values. \ No newline at end of file diff --git a/jetpack-compose/migration/migrate-xml-views-to-jetpack-compose/references/android/develop/ui/compose/migrate/interoperability-apis/compose-in-views.md b/jetpack-compose/migration/migrate-xml-views-to-jetpack-compose/references/android/develop/ui/compose/migrate/interoperability-apis/compose-in-views.md index a0bc6ad..83c7188 100644 --- a/jetpack-compose/migration/migrate-xml-views-to-jetpack-compose/references/android/develop/ui/compose/migrate/interoperability-apis/compose-in-views.md +++ b/jetpack-compose/migration/migrate-xml-views-to-jetpack-compose/references/android/develop/ui/compose/migrate/interoperability-apis/compose-in-views.md @@ -3,9 +3,10 @@ You can add Compose-based UI into an existing app that uses a View-based design. -To create a new, entirely Compose-based screen, have your activity call the `setContent()` method, and pass whatever composable functions you like. +To create a new, entirely Compose-based screen, have your +activity call the `setContent()` method, and pass whatever composable functions +you like. -
```kotlin class ExampleActivity : ComponentActivity() { @@ -24,7 +25,6 @@ class ExampleActivity : ComponentActivity() { fun Greeting(name: String) { Text(text = "Hello $name!") } - ```
@@ -32,17 +32,30 @@ fun Greeting(name: String) { This code looks just like what you'd find in a Compose-only app. > [!CAUTION] -> **Caution:** To use the `ComponentActivity.setContent` method, add the `androidx.activity:activity-compose:$latestVersion` dependency to your `build.gradle` file. +> **Caution:** To use the `ComponentActivity.setContent` +> method, add the `androidx.activity:activity-compose:$latestVersion` +> dependency to your `build.gradle` file. > -> See the [Activity releases page](https://developer.android.com/jetpack/androidx/releases/activity) to find out the latest version. +> See the [Activity releases page](https://developer.android.com/jetpack/androidx/releases/activity) +> to find out the latest version. ## `ViewCompositionStrategy` for `ComposeView` -[`ViewCompositionStrategy`](https://developer.android.com/reference/kotlin/androidx/compose/ui/platform/ViewCompositionStrategy) defines when the Composition should be disposed. The default, [`ViewCompositionStrategy.Default`](https://developer.android.com/reference/kotlin/androidx/compose/ui/platform/ViewCompositionStrategy#Default()), disposes the Composition when the underlying [`ComposeView`](https://developer.android.com/reference/kotlin/androidx/compose/ui/platform/ComposeView) detaches from the window, unless it is part of a pooling container such as a `RecyclerView`. In a single-Activity Compose-only app, this default behavior is what you would want, however, if you are incrementally adding Compose in your codebase, this behavior may cause state loss in some scenarios. +[`ViewCompositionStrategy`](https://developer.android.com/reference/kotlin/androidx/compose/ui/platform/ViewCompositionStrategy) +defines when the Composition should be disposed. The default, +[`ViewCompositionStrategy.Default`](https://developer.android.com/reference/kotlin/androidx/compose/ui/platform/ViewCompositionStrategy#Default()), +disposes the Composition when the underlying +[`ComposeView`](https://developer.android.com/reference/kotlin/androidx/compose/ui/platform/ComposeView) +detaches from the window, unless it is part of a pooling container such as a +`RecyclerView`. In a single-Activity Compose-only app, this default behavior is +what you would want, however, if you are incrementally adding Compose in your +codebase, this behavior may cause state loss in some scenarios. -To change the `ViewCompositionStrategy`, call the [`setViewCompositionStrategy()`](https://developer.android.com/reference/kotlin/androidx/compose/ui/platform/AbstractComposeView#setViewCompositionStrategy(androidx.compose.ui.platform.ViewCompositionStrategy)) method and provide a different strategy. +To change the `ViewCompositionStrategy`, call the [`setViewCompositionStrategy()`](https://developer.android.com/reference/kotlin/androidx/compose/ui/platform/AbstractComposeView#setViewCompositionStrategy(androidx.compose.ui.platform.ViewCompositionStrategy)) +method and provide a different strategy. -The table below summarizes the different scenarios you can use `ViewCompositionStrategy` in: +The table below summarizes the different scenarios you can use +`ViewCompositionStrategy` in: | `ViewCompositionStrategy` | Description and Interop Scenario | |---|---| @@ -56,7 +69,11 @@ The table below summarizes the different scenarios you can use `ViewCompositionS > [!NOTE] > **Note:** For hybrid apps with Views and Compose, that use Fragments, use `ComposeView` to wrap composables content and add to a Fragment during migration. For Compose-only apps, do not use Fragments and instead use the recommended Compose-only architecture with a single Activity and latest navigation libraries, like Navigation 3. -If you want to incorporate Compose UI content in a fragment or an existing View layout, use [`ComposeView`](https://developer.android.com/reference/kotlin/androidx/compose/ui/platform/ComposeView) and call its [`setContent()`](https://developer.android.com/reference/kotlin/androidx/compose/ui/platform/ComposeView#setContent(kotlin.Function0)) method. `ComposeView` is an Android [`View`](https://developer.android.com/reference/android/view/View). +If you want to incorporate Compose UI content in a fragment or an existing View +layout, use [`ComposeView`](https://developer.android.com/reference/kotlin/androidx/compose/ui/platform/ComposeView) +and call its +[`setContent()`](https://developer.android.com/reference/kotlin/androidx/compose/ui/platform/ComposeView#setContent(kotlin.Function0)) +method. `ComposeView` is an Android [`View`](https://developer.android.com/reference/android/view/View). You can put the `ComposeView` in your XML layout just like any other `View`: @@ -76,12 +93,13 @@ You can put the `ComposeView` in your XML layout just like any other `View`: android:layout_width="match_parent" android:layout_height="match_parent" /> - ``` -In the Kotlin source code, inflate the layout from the [layout resource](https://developer.android.com/guide/topics/resources/layout-resource) defined in XML. Then get the `ComposeView` using the XML ID, set a Composition strategy that works best for the host `View`, and call `setContent()` to use Compose. +In the Kotlin source code, inflate the layout from the [layout +resource](https://developer.android.com/guide/topics/resources/layout-resource) defined in XML. Then get the +`ComposeView` using the XML ID, set a Composition strategy that works best for +the host `View`, and call `setContent()` to use Compose. -
```kotlin class ExampleFragmentXml : Fragment() { @@ -107,14 +125,13 @@ class ExampleFragmentXml : Fragment() { return view } } - ```
-Alternatively, you can also use view binding to obtain references to the `ComposeView` by referencing the generated binding class for your XML layout file: +Alternatively, you can also use view binding to obtain references to the +`ComposeView` by referencing the generated binding class for your XML layout file: -
```kotlin class ExampleFragment : Fragment() { @@ -150,18 +167,20 @@ class ExampleFragment : Fragment() { _binding = null } } - ```
![Two slightly different text elements, one above the other](https://developer.android.com/static/develop/ui/compose/images/interop-hellos.png) -**Figure 1.** This shows the output of the code that adds Compose elements in a View UI hierarchy. The "Hello Android!" text is displayed by a `TextView` widget. The "Hello Compose!" text is displayed by a Compose text element. +**Figure 1.** This shows the output of the code that adds Compose elements in a +View UI hierarchy. The "Hello Android!" text is displayed by a +`TextView` widget. The "Hello Compose!" text is displayed by a +Compose text element. -You can also include a `ComposeView` directly in a fragment if your full screen is built with Compose, which lets you avoid using an XML layout file entirely. +You can also include a `ComposeView` directly in a fragment if your full screen +is built with Compose, which lets you avoid using an XML layout file entirely. -
```kotlin class ExampleFragmentNoXml : Fragment() { @@ -184,16 +203,15 @@ class ExampleFragmentNoXml : Fragment() { } } } - ```
## Multiple `ComposeView` instances in the same layout -If there are multiple `ComposeView` elements in the same layout, each one must have a unique ID for `savedInstanceState` to work. +If there are multiple `ComposeView` elements in the same layout, each one must +have a unique ID for `savedInstanceState` to work. -
```kotlin class ExampleFragmentMultipleComposeView : Fragment() { @@ -224,7 +242,6 @@ class ExampleFragmentMultipleComposeView : Fragment() { ) } } - ```
@@ -236,16 +253,18 @@ The `ComposeView` IDs are defined in the `res/values/ids.xml` file: - ``` ## Preview composables in Layout Editor -You can also preview composables within the Layout Editor for your XML layout containing a `ComposeView`. Doing so lets you see how your composables look within a mixed Views and Compose layout. +You can also preview composables within the Layout Editor for your XML layout +containing a `ComposeView`. Doing so lets you see how your composables look +within a mixed Views and Compose layout. -Say you want to display the following composable in the Layout Editor. Note that composables annotated with `@Preview` are good candidates to preview in the Layout Editor. +Say you want to display the following composable in the Layout Editor. Note +that composables annotated with `@Preview` are good candidates to preview in the +Layout Editor. -
```kotlin @Preview @@ -253,12 +272,13 @@ Say you want to display the following composable in the Layout Editor. Note that fun GreetingPreview() { Greeting(name = "Android") } - ```
-To display this composable, use the `tools:composableName` tools attribute and set its value to the fully qualified name of the composable to preview in the layout. +To display this composable, use the `tools:composableName` tools attribute and +set its value to the fully qualified name of the composable to preview in the +layout. ```xml - ``` ![Composable displayed within layout editor](https://developer.android.com/static/develop/ui/compose/images/layout-editor-composable-preview.png) \ No newline at end of file diff --git a/jetpack-compose/migration/migrate-xml-views-to-jetpack-compose/references/android/develop/ui/compose/migrate/interoperability-apis/views-in-compose.md b/jetpack-compose/migration/migrate-xml-views-to-jetpack-compose/references/android/develop/ui/compose/migrate/interoperability-apis/views-in-compose.md index 248d909..14b9121 100644 --- a/jetpack-compose/migration/migrate-xml-views-to-jetpack-compose/references/android/develop/ui/compose/migrate/interoperability-apis/views-in-compose.md +++ b/jetpack-compose/migration/migrate-xml-views-to-jetpack-compose/references/android/develop/ui/compose/migrate/interoperability-apis/views-in-compose.md @@ -1,13 +1,22 @@ -You can include an Android View hierarchy in a Compose UI. This approach is particularly useful if you want to use UI elements that are not yet available in Compose, like [`AdView`](https://developers.google.com/android/reference/com/google/android/gms/ads/AdView). This approach also lets you reuse custom views you may have designed. +You can include an Android View hierarchy in a Compose UI. This approach is +particularly useful if you want to use UI elements that are not yet available in +Compose, like +[`AdView`](https://developers.google.com/android/reference/com/google/android/gms/ads/AdView). +This approach also lets you reuse custom views you may have designed. > [!NOTE] > **Note:** Use `AndroidView` as a wrapper only for missing SDK components without Compose support. Rewrite your custom Views in Compose wherever possible, starting the migration with the simplest custom Views and scaling to more complex ones.
-To include a view element or hierarchy, use the [`AndroidView`](https://developer.android.com/reference/kotlin/androidx/compose/ui/viewinterop/AndroidView.composable#AndroidView(kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function1)) composable. `AndroidView` is passed a lambda that returns a [`View`](https://developer.android.com/reference/android/view/View). `AndroidView` also provides an `update` callback that is called when the view is inflated. The `AndroidView` recomposes whenever a `State` read within the callback changes. `AndroidView`, like many other built-in composables, takes a `Modifier` parameter that can be used, for example, to set its position in the parent composable. +To include a view element or hierarchy, use the [`AndroidView`](https://developer.android.com/reference/kotlin/androidx/compose/ui/viewinterop/AndroidView.composable#AndroidView(kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function1)) +composable. `AndroidView` is passed a lambda that returns a +[`View`](https://developer.android.com/reference/android/view/View). `AndroidView` also provides an `update` +callback that is called when the view is inflated. The `AndroidView` recomposes +whenever a `State` read within the callback changes. `AndroidView`, like many +other built-in composables, takes a `Modifier` parameter that can be used, for +example, to set its position in the parent composable. -
```kotlin @Composable @@ -45,8 +54,6 @@ fun ContentExample() { CustomView() } } - - ```
@@ -56,12 +63,14 @@ fun ContentExample() { ## `AndroidView` with view binding -To embed an XML layout, use the [`AndroidViewBinding`](https://developer.android.com/reference/kotlin/androidx/compose/ui/viewinterop/package-summary#AndroidViewBinding(kotlin.Function3,%20androidx.compose.ui.Modifier,%20kotlin.Function1)) API, which is provided by the `androidx.compose.ui:ui-viewbinding` library. To do this, your project must enable [view binding](https://developer.android.com/topic/libraries/view-binding#setup). +To embed an XML layout, use the +[`AndroidViewBinding`](https://developer.android.com/reference/kotlin/androidx/compose/ui/viewinterop/package-summary#AndroidViewBinding(kotlin.Function3,%20androidx.compose.ui.Modifier,%20kotlin.Function1)) +API, which is provided by the `androidx.compose.ui:ui-viewbinding` library. To +do this, your project must enable [view binding](https://developer.android.com/topic/libraries/view-binding#setup). > [!NOTE] > **Note:** For Compose-only apps, don't use `AndroidViewBinding` to inflate full screen-level XML layouts, and instead use it only for smaller, legacy XML layouts during the incremental migration process. -
```kotlin @Composable @@ -70,21 +79,23 @@ fun AndroidViewBindingExample() { exampleView.setBackgroundColor(Color.GRAY) } } - ```
## `AndroidView` in Lazy lists -If you are using an `AndroidView` in a Lazy list (`LazyColumn`, `LazyRow`, `Pager`, etc.), consider using the [`AndroidView`](https://developer.android.com/reference/kotlin/androidx/compose/ui/viewinterop/package-summary#AndroidView(kotlin.Function1,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function1,kotlin.Function1)) overload introduced in version 1.4.0-rc01. This overload allows Compose to reuse the underlying `View` instance when the containing composition is reused as is the case for Lazy lists. +If you are using an `AndroidView` in a Lazy list (`LazyColumn`, `LazyRow`, +`Pager`, etc.), consider using the [`AndroidView`](https://developer.android.com/reference/kotlin/androidx/compose/ui/viewinterop/package-summary#AndroidView(kotlin.Function1,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function1,kotlin.Function1)) +overload introduced in version 1.4.0-rc01. This overload allows Compose to reuse +the underlying `View` instance when the containing composition is reused as is +the case for Lazy lists. This overload of `AndroidView` adds 2 additional parameters: - `onReset` - A callback invoked to signal that the `View` is about to be reused. This must be non-null to enable View reuse. - `onRelease` (optional) - A callback invoked to signal that the `View` has exited the composition and will not be reused again. -
```kotlin @Composable @@ -106,50 +117,72 @@ fun AndroidViewInLazyList() { } } } - ```
## Fragments in Compose (Transitionary Step) -Use the `AndroidFragment` composable to add a `Fragment` in Compose. `AndroidFragment` has fragment-specific handling such as removing the fragment when the composable leaves the composition. +Use the `AndroidFragment` composable to add a `Fragment` in Compose. +`AndroidFragment` has fragment-specific handling such as removing the +fragment when the composable leaves the composition. > [!NOTE] > **Note:** Wrap existing Fragments in Compose only during incremental migration. For Compose-only apps, do not use Fragments and instead use the recommended Compose-only architecture with a single Activity and latest navigation libraries, like Navigation 3. -To include a fragment, use the [`AndroidFragment`](https://developer.android.com/reference/kotlin/androidx/fragment/compose/package-summary#AndroidFragment) composable. You pass a `Fragment` class to `AndroidFragment`, which then adds an instance of that class directly into the composition. `AndroidFragment` also provides a `fragmentState` object to create the `AndroidFragment` with a given state, `arguments` to pass into the new fragment, and an `onUpdate` callback that provides the fragment from the composition. Like many other built-in composables, `AndroidFragment` accepts a `Modifier` parameter that you can use, for example, to set its position in the parent composable. +To include a fragment, use the [`AndroidFragment`](https://developer.android.com/reference/kotlin/androidx/fragment/compose/package-summary#AndroidFragment) +composable. You pass a `Fragment` class to `AndroidFragment`, which then adds +an instance of that class directly into the composition. `AndroidFragment` also +provides a `fragmentState` object to create the `AndroidFragment` with a given +state, `arguments` to pass into the new fragment, and an `onUpdate` callback +that provides the fragment from the composition. Like many +other built-in composables, `AndroidFragment` accepts a `Modifier` parameter +that you can use, for +example, to set its position in the parent composable. Call `AndroidFragment` in Compose as follows: -
```kotlin @Composable fun FragmentInComposeExample() { AndroidFragment() } - - ```
## Calling the Android framework from Compose -Compose operates within the Android framework classes. For example, it's hosted on Android View classes, like `Activity` or `Fragment`, and might use Android framework classes like the `Context`, system resources, `Service`, or `BroadcastReceiver`. +Compose operates within the Android framework classes. For example, it's hosted +on Android View classes, like `Activity` or `Fragment`, and might use Android +framework classes like the `Context`, system resources, +`Service`, or `BroadcastReceiver`. To learn more about system resources, see [Resources in Compose](https://developer.android.com/develop/ui/compose/resources). ### Composition Locals -[`CompositionLocal`](https://developer.android.com/reference/kotlin/androidx/compose/runtime/CompositionLocal) classes allow passing data implicitly through composable functions. They're usually provided with a value in a certain node of the UI tree. That value can be used by its composable descendants without declaring the `CompositionLocal` as a parameter in the composable function. +[`CompositionLocal`](https://developer.android.com/reference/kotlin/androidx/compose/runtime/CompositionLocal) +classes allow passing data implicitly through composable functions. They're +usually provided with a value in a certain node of the UI tree. That value can +be used by its composable descendants without declaring the `CompositionLocal` +as a parameter in the composable function. -`CompositionLocal` is used to propagate values for Android framework types in Compose such as `Context`, `Configuration` or the `View` in which the Compose code is hosted with the corresponding [`LocalContext`](https://developer.android.com/reference/kotlin/androidx/compose/ui/platform/package-summary#LocalContext()), [`LocalConfiguration`](https://developer.android.com/reference/kotlin/androidx/compose/ui/platform/package-summary#LocalConfiguration()), or [`LocalView`](https://developer.android.com/reference/kotlin/androidx/compose/ui/platform/package-summary#LocalView()). Note that `CompositionLocal` classes are prefixed with `Local` for better discoverability with auto-complete in the IDE. +`CompositionLocal` is used to propagate values for Android framework types in +Compose such as `Context`, `Configuration` or the `View` in which the Compose +code is hosted with the corresponding +[`LocalContext`](https://developer.android.com/reference/kotlin/androidx/compose/ui/platform/package-summary#LocalContext()), +[`LocalConfiguration`](https://developer.android.com/reference/kotlin/androidx/compose/ui/platform/package-summary#LocalConfiguration()), +or +[`LocalView`](https://developer.android.com/reference/kotlin/androidx/compose/ui/platform/package-summary#LocalView()). +Note that `CompositionLocal` classes are prefixed with `Local` for better +discoverability with auto-complete in the IDE. -Access the current value of a `CompositionLocal` by using its `current` property. For example, the code below shows a toast message by providing `LocalContext.current` into the `Toast.makeToast` method. +Access the current value of a `CompositionLocal` by using its `current` +property. For example, the code below shows a toast message by providing +`LocalContext.current` into the `Toast.makeToast` method. -
```kotlin @Composable @@ -161,16 +194,18 @@ fun ToastGreetingButton(greeting: String) { Text("Greet") } } - ```
### Broadcast receivers -To showcase `CompositionLocal` and [side effects](https://developer.android.com/develop/ui/compose/side-effects), if a [`BroadcastReceiver`](https://developer.android.com/guide/components/broadcasts) needs to be registered from a composable function, use of `LocalContext` to use the current context, and `rememberUpdatedState` and `DisposableEffect` side effects. +To showcase `CompositionLocal` and [side +effects](https://developer.android.com/develop/ui/compose/side-effects), if a +[`BroadcastReceiver`](https://developer.android.com/guide/components/broadcasts) needs to be registered from +a composable function, use of `LocalContext` to use the current context, and +`rememberUpdatedState` and `DisposableEffect` side effects. -
```kotlin @Composable @@ -212,16 +247,18 @@ fun HomeScreen() { /* Rest of the HomeScreen */ } - ```
## Other interactions -If there isn't a utility defined for the interaction you need, the best practice is to follow the general Compose guideline, *data flows down, events flow up* (discussed at more length in [Thinking in Compose](https://developer.android.com/develop/ui/compose/mental-model)). For example, this composable launches a different activity: +If there isn't a utility defined for the interaction you need, the best practice +is to follow the general Compose guideline, +*data flows down, events flow up* (discussed at more length in [Thinking +in Compose](https://developer.android.com/develop/ui/compose/mental-model)). For example, this composable +launches a different activity: -
```kotlin class OtherInteractionsActivity : ComponentActivity() { @@ -244,7 +281,6 @@ fun ExampleComposable(data: DataExample, onButtonClick: () -> Unit) { Text(data.title) } } - ```
\ No newline at end of file diff --git a/jetpack-compose/migration/migrate-xml-views-to-jetpack-compose/references/android/develop/ui/compose/setup-compose-dependencies-and-compiler.md b/jetpack-compose/migration/migrate-xml-views-to-jetpack-compose/references/android/develop/ui/compose/setup-compose-dependencies-and-compiler.md index ceda68a..657687a 100644 --- a/jetpack-compose/migration/migrate-xml-views-to-jetpack-compose/references/android/develop/ui/compose/setup-compose-dependencies-and-compiler.md +++ b/jetpack-compose/migration/migrate-xml-views-to-jetpack-compose/references/android/develop/ui/compose/setup-compose-dependencies-and-compiler.md @@ -1,7 +1,9 @@ -For Gradle, use the Compose Compiler Gradle plugin to set up and configure Compose. +For Gradle, use the Compose Compiler Gradle plugin to set up and configure +Compose. > [!NOTE] -> **Note:** The Compose Compiler Gradle Plugin is only available from Kotlin 2.0+. For migration instructions, see ["Jetpack Compose compiler moving to the Kotlin repository"](https://android-developers.googleblog.com/2024/04/jetpack-compose-compiler-moving-to-kotlin-repository.html). +> **Note:** The Compose Compiler Gradle Plugin is only available from Kotlin 2.0+. For migration instructions, see ["Jetpack Compose compiler moving to the Kotlin +> repository"](https://android-developers.googleblog.com/2024/04/jetpack-compose-compiler-moving-to-kotlin-repository.html). ### Set up with Gradle version catalogs @@ -19,7 +21,8 @@ Set up the Compose Compiler Gradle plugin: // Add this line compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } -3. In the project's root `build.gradle.kts` file, add the following to the `plugins` section. +3. In the project's root `build.gradle.kts` file, add the following to the + `plugins` section. plugins { // Existing plugins @@ -33,11 +36,14 @@ Set up the Compose Compiler Gradle plugin: alias(libs.plugins.compose.compiler) } -The project should now build and compile if it was using the default set up. If it had configured custom options on the Compose compiler, follow the next section. +The project should now build and compile if it was using the default set up. If +it had configured custom options on the Compose compiler, follow the next +section. ### Set up the Compose Compiler without Gradle version catalogs -Add the plugin to `build.gradle.kts` files associated with modules where Compose is used: +Add the plugin to `build.gradle.kts` files associated with modules where Compose +is used: plugins { id("org.jetbrains.kotlin.plugin.compose") version "2.3.21" // this version matches your Kotlin version @@ -53,7 +59,9 @@ Add the classpath to your top-level project `build.gradle.kts` file: ### Configuration options with the Compose Compiler Gradle Plugin -To configure the Compose compiler using the Gradle plugin, add the `composeCompiler` block to the module's `build.gradle.kts` file at the top level: +To configure the Compose compiler using the Gradle plugin, add the +`composeCompiler` block to the module's `build.gradle.kts` file at the top +level: android { ... } @@ -66,9 +74,10 @@ For the full list of available options, see the [documentation](https://www.jetb ## Set up Compose dependencies -Always use the latest Compose BOM version: `2026.06.01`. +Always use the latest Compose BOM version: `2026.08.00`. -Set the `compose` flag to `true` inside the Android [`BuildFeatures`](https://developer.android.com/reference/tools/gradle-api/7.0/com/android/build/api/dsl/BuildFeatures) to enable [Compose functionality](https://developer.android.com/develop/ui/compose/tooling) in Android Studio. +Set the `compose` flag to `true` inside the Android [`BuildFeatures`](https://developer.android.com/reference/tools/gradle-api/7.0/com/android/build/api/dsl/BuildFeatures) to +enable [Compose functionality](https://developer.android.com/develop/ui/compose/tooling) in Android Studio. Add the following definition to your app's `build.gradle` file: @@ -94,7 +103,7 @@ Add the Compose BOM and the subset of Compose library dependencies: dependencies { - def composeBom = platform('androidx.compose:compose-bom:2026.06.01') + def composeBom = platform('androidx.compose:compose-bom:2026.08.00') implementation composeBom androidTestImplementation composeBom @@ -133,7 +142,7 @@ Add the Compose BOM and the subset of Compose library dependencies: dependencies { - val composeBom = platform("androidx.compose:compose-bom:2026.06.01") + val composeBom = platform("androidx.compose:compose-bom:2026.08.00") implementation(composeBom) androidTestImplementation(composeBom) @@ -169,12 +178,20 @@ Add the Compose BOM and the subset of Compose library dependencies: } > [!NOTE] -> **Note:** Jetpack Compose is shipped using a Bill of Materials (BOM), to keep the versions of all library groups in sync. Read more about it in the [Bill of Materials page](https://developer.android.com/develop/ui/compose/bom/bom). +> **Note:** Jetpack Compose is shipped using a Bill of Materials (BOM), to keep the versions of all library groups in sync. Read more about it in the [Bill of +> Materials page](https://developer.android.com/develop/ui/compose/bom/bom). ## `compileSdk` and Android Gradle Plugin compatibility -Compose library releases continually adopt the latest `compileSdk` versions to provide access to the latest Android features. Newer `compileSdk` versions require newer versions of Android Gradle Plugin, so adopting new Compose releases also requires projects to adopt new versions of the Android Gradle Plugin. We recommend keeping your project's `compileSdk` up to date with the latest released versions. `compileSdk` is unrelated from `targetSdk`. +Compose library releases continually adopt the latest `compileSdk` versions to +provide access to the latest Android features. Newer `compileSdk` versions +require newer versions of Android Gradle Plugin, so adopting new Compose +releases also requires projects to adopt new versions of the Android Gradle +Plugin. We recommend keeping your project's `compileSdk` up to date with the +latest released versions. `compileSdk` is unrelated from `targetSdk`. -For example, starting with Compose 1.12.0, projects are required to use `compileSdk 37` and Android Gradle Plugin (AGP) 9. +For example, starting with Compose 1.12.0, projects are required to use +`compileSdk 37` and Android Gradle Plugin (AGP) 9. -To check which version of AGP is supported for different API levels, see the [Android Gradle plugin API level support](https://developer.android.com/build/releases/about-agp#api-level-support) documentation. \ No newline at end of file +To check which version of AGP is supported for different API levels, see the +[Android Gradle plugin API level support](https://developer.android.com/build/releases/about-agp#api-level-support) documentation. \ No newline at end of file diff --git a/jetpack-compose/migration/migrate-xml-views-to-jetpack-compose/references/identify-optimal-xml-candidate.md b/jetpack-compose/migration/migrate-xml-views-to-jetpack-compose/references/identify-optimal-xml-candidate.md index e8a42d0..650ac92 100644 --- a/jetpack-compose/migration/migrate-xml-views-to-jetpack-compose/references/identify-optimal-xml-candidate.md +++ b/jetpack-compose/migration/migrate-xml-views-to-jetpack-compose/references/identify-optimal-xml-candidate.md @@ -14,12 +14,18 @@ Prioritize layouts that meet the following criteria: ### 3. Risk assessment -Evaluate the migration risk based on: \* **Reusability:** Find layouts with **minimum reuse** across the project to limit regression impact. \* **Accessibility:** Ensure the layout has an **easily accessible entry point** (e.g., used in a simple Activity, Fragment, or as a standalone include). +Evaluate the migration risk based on: +\* **Reusability:** Find layouts with **minimum reuse** across the project to limit regression impact. +\* **Accessibility:** Ensure the layout has an **easily accessible entry point** (e.g., used in a simple Activity, Fragment, or as a standalone include). *** ** * ** *** ## Output requirements -Provide a ranked list of the top 3-5 candidates. For each candidate, include: 1. **File path:** (e.g., `res/layout/item_user_profile.xml`) 2. **Rationale:** Why this is a good candidate based on the provided criteria. 3. **Complexity score:** A rating from 1-5 (1 being simplest). 4. **Dependency count:** List of custom/external views found within. +Provide a ranked list of the top 3-5 candidates. For each candidate, include: +1. **File path:** (e.g., `res/layout/item_user_profile.xml`) +2. **Rationale:** Why this is a good candidate based on the provided criteria. +3. **Complexity score:** A rating from 1-5 (1 being simplest). +4. **Dependency count:** List of custom/external views found within. **Action:** If you support user interaction, ask the user to choose which XML to proceed with. Else proceed with the best option, based on the previous criteria. \ No newline at end of file diff --git a/jetpack-compose/migration/migrate-xml-views-to-jetpack-compose/references/xml-layout-migration.md b/jetpack-compose/migration/migrate-xml-views-to-jetpack-compose/references/xml-layout-migration.md index ba27ac4..5aeb6b3 100644 --- a/jetpack-compose/migration/migrate-xml-views-to-jetpack-compose/references/xml-layout-migration.md +++ b/jetpack-compose/migration/migrate-xml-views-to-jetpack-compose/references/xml-layout-migration.md @@ -1,6 +1,7 @@ ## 1. Structural analysis \& mapping -**Identify the precise mapping** between XML elements and Compose equivalents. You must determine: +**Identify the precise mapping** between XML elements and Compose equivalents. +You must determine: - The exact `@Composable` functions (e.g., `ConstraintLayout`, `Column`, `LazyColumn`) that replace the XML tag hierarchy. - The specific parameters and `Modifier` extensions required to replicate XML attributes (e.g., `layout_width`, `padding`, `elevation`). @@ -8,7 +9,9 @@ ## 2. Migration execution -**Convert the XML layout code to Jetpack Compose**, ensuring the visual hierarchy and layout logic are preserved while leveraging Compose's declarative nature. +**Convert the XML layout code to Jetpack Compose**, ensuring the visual +hierarchy and layout logic are preserved while leveraging Compose's declarative +nature. ## 3. Theming \& design system integrity @@ -20,7 +23,8 @@ ## 4. Component layering \& reusability -Evaluate if the XML layout serves as a foundation-level design system component (reused across the app with a distinct role). If it is: +Evaluate if the XML layout serves as a foundation-level design system component +(reused across the app with a distinct role). If it is: - **Create a reusable composable:** Do not just inline the code. Define a new standalone `@Composable`. - **Parameterization:** Expose specific parameters for variable data (text, colors, styles) and use `Modifier` for layout-specific customizations. @@ -40,7 +44,6 @@ Example before migration: Example after migration: -
```kotlin @Composable @@ -70,7 +73,6 @@ fun RoundedBorderlessButton( ) } } - ```
diff --git a/jetpack-compose/theming/styles/SKILL.md b/jetpack-compose/theming/styles/SKILL.md index eb97976..e2790b6 100644 --- a/jetpack-compose/theming/styles/SKILL.md +++ b/jetpack-compose/theming/styles/SKILL.md @@ -8,7 +8,7 @@ description: Use this skill to integrate the Jetpack Compose Styles API into an license: Complete terms in LICENSE.txt metadata: author: Google LLC - last-updated: '2026-08-06' + last-updated: '2026-08-14' keywords: - Jetpack Compose - Styles @@ -30,11 +30,13 @@ metadata: - The project must use `compileSdk` version 37 or higher. - The project must use `androidx.compose.foundation:foundation` version `1.12.0-alpha01` or higher. - Alternatively, the project must use Compose BOM version `2026.04.01` or higher. -- The API requires this exact package: `import androidx.compose.foundation.style.Style` +- The API requires this exact package: `import + androidx.compose.foundation.style.Style` ### 2. Configure compiler options to enable experimental API -You must opt-in to the experimental API at the project level. Add the following block to your module's `build.gradle.kts`: +You must opt-in to the experimental API at the project level. Add the following +block to your module's `build.gradle.kts`: kotlin { compilerOptions { @@ -47,9 +49,12 @@ You must opt-in to the experimental API at the project level. Add the following Refer to the official documentation to complete specific development tasks: -- Basic Style Usage: To set backgrounds, sizes, and alignments on a component, follow the [Compose Styles Fundamentals Guide](references/android/develop/ui/compose/styles/fundamentals.md). -- State and Transitions: To configure property changes for state shifts (like pressed or hovered), follow the [Animations and State-Based Styling Guide](references/android/develop/ui/compose/styles/state-animations.md). -- Architecture Trade offs: To decide when to use a Style versus a standard Modifier, follow the [Styles versus Modifiers Comparison](references/android/develop/ui/compose/styles/styles-vs-modifiers.md). +- Basic Style Usage: To set backgrounds, sizes, and alignments on a component, follow the [Compose Styles Fundamentals + Guide](references/android/develop/ui/compose/styles/fundamentals.md). +- State and Transitions: To configure property changes for state shifts (like pressed or hovered), follow the [Animations and State-Based Styling + Guide](references/android/develop/ui/compose/styles/state-animations.md). +- Architecture Trade offs: To decide when to use a Style versus a standard Modifier, follow the [Styles versus Modifiers + Comparison](references/android/develop/ui/compose/styles/styles-vs-modifiers.md). - Theme Level Integration: To connect style definitions with custom themes, follow [Theming with Styles](references/android/develop/ui/compose/styles/theming.md) and [Custom Themes in Compose](references/android/develop/ui/compose/designsystems/custom.md). ## Step-by-Step Migration Workflow @@ -66,7 +71,6 @@ Refer to the official documentation to complete specific development tasks: 1. Create a new file named `ComponentStyles.kt` in your theme directory. 2. Define a top-level data class to hold your component styles, for example, the Jetsnack one is called `JetsnackStyles`: -
```kotlin object ExampleComponentStyles { @@ -77,14 +81,13 @@ Refer to the official documentation to complete specific development tasks: } } - ```
-3. Expose this class through your custom theme with a static reference, don't use `CompositionLocals` here as it's not required. +3. Expose this class through your custom theme with a static reference, don't + use `CompositionLocals` here as it's not required. -
```kotlin @Immutable @@ -101,14 +104,13 @@ Refer to the official documentation to complete specific development tasks: val styles: ComponentStyles = ComponentStyles } } - ```
-4. Provide extensions on `StyleScope` to reference theme tokens directly if they are exposed using `CompositionLocals`. For example: +4. Provide extensions on `StyleScope` to reference theme tokens directly if + they are exposed using `CompositionLocals`. For example: -
```kotlin val StyleScope.colors: JetsnackColors @@ -119,15 +121,14 @@ Refer to the official documentation to complete specific development tasks: val StyleScope.shapes: Shapes get() = LocalJetsnackTheme.currentValue.shapes - - ```
### Step 3: Migrate a component to Styles API -For each custom component (for example, `CustomButton`), complete the following sequence: +For each custom component (for example, `CustomButton`), complete the following +sequence: 1. **Establish a visual baseline (If an emulator is available):** - **If you CANNOT run an Android emulator:** Skip this step entirely and proceed to Step 2. @@ -136,7 +137,8 @@ For each custom component (for example, `CustomButton`), complete the following - **Option B (If no test exists):** Create a test using the project's existing testing framework, then run it. - **Option C (If no framework exists):** Create a minimal screenshot test using UI Automator or Espresso, then run it. 2. **Remove individual styling parameters** : Remove styling parameters such as `backgroundColor`, `shape`, `textStyle`, and `contentPadding` from the signature - anything that `StyleScope` supports. -3. **Add the style parameter** : Add `style: Style = Style` to the function signature. Always ensure the default value is exactly `Style` (e.g., `style: Style = Style`) and not a specific style default like `ChipStyleDefault` or any other value. +3. **Add the style parameter** : Add `style: Style = Style` to the function signature. Always ensure the default value is exactly `Style` (e.g., `style: + Style = Style`) and not a specific style default like `ChipStyleDefault` or any other value. 4. **Declare state tracking** : If the component is interactable, create a `MutableStyleState` using the interaction source. Update state fields (such as `isEnabled`) inside the Composable to track the state correctly. 5. **Apply styleable modifier** : Replace specific layout modifiers on the root element with `Modifier.styleable()`. 6. **Move defaults to ComponentStyles** : Move hardcoded values from the component definition to a dedicated `Style` instance in `ComponentStyles.kt`. @@ -146,7 +148,6 @@ For each custom component (for example, `CustomButton`), complete the following Before Migration: -
```kotlin @Composable @@ -171,14 +172,12 @@ fun CustomButton( content = content, ) } - ```
After Migration: -
```kotlin // Exposed via ComponentStyles.kt @@ -216,7 +215,6 @@ fun CustomButton( content = content, ) } - ```
diff --git a/jetpack-compose/theming/styles/references/android/develop/ui/compose/designsystems/custom.md b/jetpack-compose/theming/styles/references/android/develop/ui/compose/designsystems/custom.md index ae05b5a..59dbb25 100644 --- a/jetpack-compose/theming/styles/references/android/develop/ui/compose/designsystems/custom.md +++ b/jetpack-compose/theming/styles/references/android/develop/ui/compose/designsystems/custom.md @@ -1,4 +1,7 @@ -While Material is our recommended design system and Jetpack Compose ships an implementation of Material, you are not forced to use it. Material is built entirely on public APIs, so it's possible to create your own design system in the same manner. +While Material is our recommended design system and Jetpack Compose ships an +implementation of Material, you are not forced to use it. Material is built +entirely on public APIs, so it's possible to create your own design system in +the same manner. There are several approaches you might take: @@ -6,15 +9,21 @@ There are several approaches you might take: - [Replace one or more Material systems](https://developer.android.com/develop/ui/compose/designsystems/custom#replacing-systems) --- `Colors`, `Typography`, or `Shapes` --- with custom implementations while keeping the others. - [Implement a fully custom design system](https://developer.android.com/develop/ui/compose/designsystems/custom#implementing-fully-custom) to replace `MaterialTheme`. -You may also want to continue using Material components with a custom design system. It's possible to do this but there are things to keep in mind to suit the approach you've taken. +You may also want to continue using Material components with a custom design +system. It's possible to do this but there are things to keep in mind to suit +the approach you've taken. -To learn more about the lower-level constructs and APIs used by `MaterialTheme` and custom design systems, check out the [Anatomy of a theme in Compose](https://developer.android.com/develop/ui/compose/designsystems/anatomy) guide. +To learn more about the lower-level constructs and APIs used by `MaterialTheme` +and custom design systems, check out the [Anatomy of a theme in Compose](https://developer.android.com/develop/ui/compose/designsystems/anatomy) guide. ## Extend Material Theming -Compose Material closely models [Material Theming](https://m3.material.io/) to make it straightforward and type-safe to follow the Material guidelines. However, it's possible to extend the color, typography, and shape sets with additional values. The simplest approach is to add extension properties: +Compose Material closely models +[Material Theming](https://m3.material.io/) +to make it straightforward and type-safe to follow the Material guidelines. +However, it's possible to extend the color, typography, and shape sets with +additional values. The simplest approach is to add extension properties: -
```kotlin // Use with MaterialTheme.colorScheme.snackbarAction @@ -29,21 +38,26 @@ val Typography.textFieldInput: TextStyle // Use with MaterialTheme.shapes.card val Shapes.card: Shape get() = RoundedCornerShape(size = 20.dp) - ```
-This provides consistency with `MaterialTheme` usage APIs. An example of this defined by Compose itself is [`surfaceColorAtElevation`](https://developer.android.com/reference/kotlin/androidx/compose/material3/package-summary#(androidx.compose.material3.ColorScheme).surfaceColorAtElevation(androidx.compose.ui.unit.Dp)), which determines the surface color that should be used depending on the elevation. +This provides consistency with `MaterialTheme` usage APIs. An example of this +defined by Compose itself is +[`surfaceColorAtElevation`](https://developer.android.com/reference/kotlin/androidx/compose/material3/package-summary#(androidx.compose.material3.ColorScheme).surfaceColorAtElevation(androidx.compose.ui.unit.Dp)), +which determines the surface color that should be used depending on the +elevation. > [!NOTE] > **Note:** This approach is only recommended for straightforward theming value additions, or for values that are the same in different themes. If you have multiple themes, it's better to define a class with new properties instead. -Another approach is to define an extended theme that "wraps" `MaterialTheme` and its values. +Another approach is to define an extended theme that "wraps" `MaterialTheme` and +its values. -Suppose you want to add two additional colors --- `caution` and `onCaution`, a yellow color used for actions that are semi-dangerous --- whilst keeping the existing Material colors: +Suppose you want to add two additional colors --- `caution` and `onCaution`, a +yellow color used for actions that are semi-dangerous --- whilst keeping the +existing Material colors: -
```kotlin @Immutable @@ -82,20 +96,22 @@ object ExtendedTheme { @Composable get() = LocalExtendedColors.current } - ```
-This is similar to `MaterialTheme` usage APIs. It also supports multiple themes as you can nest `ExtendedTheme`s in the same way as `MaterialTheme`. +This is similar to `MaterialTheme` usage APIs. It also supports multiple themes +as you can nest `ExtendedTheme`s in the same way as `MaterialTheme`. ### Use Material components -When extending Material Theming, existing `MaterialTheme` values are maintained and Material components still have reasonable defaults. +When extending Material Theming, existing `MaterialTheme` values are maintained +and Material components still have reasonable defaults. -If you want to use extended values in components, wrap them in your own composable functions, directly setting the values you want to alter, and exposing others as parameters to the containing composable: +If you want to use extended values in components, wrap them in your own +composable functions, directly setting the values you want to alter, and +exposing others as parameters to the containing composable: -
```kotlin @Composable @@ -115,14 +131,13 @@ fun ExtendedButton( content = content ) } - ```
-You would then replace usages of `Button` with `ExtendedButton` where appropriate. +You would then replace usages of `Button` with `ExtendedButton` where +appropriate. -
```kotlin @Composable @@ -134,18 +149,19 @@ fun ExtendedApp() { } } } - ```
## Replace Material subsystems -Instead of extending Material Theming, you may want to replace one or more systems --- `Colors`, `Typography`, or `Shapes` --- with a custom implementation, while maintaining the others. +Instead of extending Material Theming, you may want to replace one or more +systems --- `Colors`, `Typography`, or `Shapes` --- with a custom implementation, +while maintaining the others. -Suppose you want to replace the type and shape systems while keeping the color system: +Suppose you want to replace the type and shape systems while keeping the color +system: -
```kotlin @Immutable @@ -206,21 +222,22 @@ object ReplacementTheme { @Composable get() = LocalReplacementShapes.current } - ```
### Use Material components -When one or more systems of `MaterialTheme` have been replaced, using Material components as-is may result in unwanted Material color, type, or shape values. +When one or more systems of `MaterialTheme` have been replaced, using Material +components as-is may result in unwanted Material color, type, or shape values. -If you want to use replacement values in components, wrap them in your own composable functions, directly setting the values for the relevant system, and exposing others as parameters to the containing composable. +If you want to use replacement values in components, wrap them in your own +composable functions, directly setting the values for the relevant system, and +exposing others as parameters to the containing composable. > [!NOTE] > **Note:** Not all values may be exposed as parameters in Material composables, in particular with `CompositionLocal` composables (such as `LocalTextStyle`). In such cases you may need to wrap `content` lambdas in provider functions (like `ProvideTextStyle`). -
```kotlin @Composable @@ -242,14 +259,13 @@ fun ReplacementButton( } ) } - ```
-You would then replace usages of `Button` with `ReplacementButton` where appropriate. +You would then replace usages of `Button` with `ReplacementButton` where +appropriate. -
```kotlin @Composable @@ -261,28 +277,33 @@ fun ReplacementApp() { } } } - ```
## Implement a fully custom design system -You may want to replace Material Theming with a fully custom design system. Consider that `MaterialTheme` provides the following systems: +You may want to replace Material Theming with a fully custom design system. +Consider that `MaterialTheme` provides the following systems: - `Colors`, `Typography`, and `Shapes`: Material Theming systems - `TextSelectionColors`: Colors used for text selection by `Text` and `TextField` - `Ripple` and `RippleTheme`: Material implementation of `Indication` -If you want to continue using Material components, you must replace some of these systems in your custom themes or handle the systems in your components to avoid unwanted behavior. +If you want to continue using Material components, you must replace some of +these systems in your custom themes or handle the systems in your +components to avoid unwanted behavior. -However, design systems are not limited to the concepts Material relies on. You can modify existing systems and introduce entirely new ones --- with new classes and types --- to make other concepts compatible with themes. +However, design systems are not limited to the concepts Material relies on. You +can modify existing systems and introduce entirely new ones --- with new classes +and types --- to make other concepts compatible with themes. -In the following code, we model a custom color system that includes gradients (`List`), include a type system, introduce a new elevation system, and exclude other systems provided by `MaterialTheme`: +In the following code, we model a custom color system that includes gradients +(`List`), include a type system, introduce a new elevation system, +and exclude other systems provided by `MaterialTheme`: ![Screenshot of a mobile app UI demonstrating a custom design system with elements using gradients for colors, custom typography, and elevation.](https://developer.android.com/static/develop/ui/compose/images/themes/custom-color-gradients.png) -
```kotlin @Immutable @@ -362,20 +383,23 @@ object CustomTheme { @Composable get() = LocalCustomElevation.current } - ```
### Use Material components -When no `MaterialTheme` is present, using Material components as-is will result in unwanted Material color, type, and shape values and indication behavior. +When no `MaterialTheme` is present, using Material components as-is will result +in unwanted Material color, type, and shape values and indication behavior. -If you want to use custom values in components, wrap them in your own composable functions, directly setting the values for the relevant system, and exposing others as parameters to the containing composable. +If you want to use custom values in components, wrap them in your own composable +functions, directly setting the values for the relevant system, and exposing +others as parameters to the containing composable. -We recommend that you access values you set from your custom theme. Alternatively, if your theme doesn't provide `Color`, `TextStyle`, `Shape`, or other systems, you can hardcode them. +We recommend that you access values you set from your custom theme. +Alternatively, if your theme doesn't provide `Color`, `TextStyle`, `Shape`, or +other systems, you can hardcode them. -
```kotlin @Composable @@ -414,7 +438,6 @@ fun CustomButton( } val ButtonShape = RoundedCornerShape(percent = 50) - ```
@@ -422,7 +445,11 @@ val ButtonShape = RoundedCornerShape(percent = 50) > [!NOTE] > **Note:** `Button` uses `rememberRipple()` internally to provide a `Ripple` `Indication`. It's a good idea to check the source code when implementing other custom components that wrap existing components. -If you've introduced new class types --- such as `List` to represent gradients --- then it may be better to implement components from scratch instead of wrapping them. For an example, take a look at [`JetsnackButton`](https://github.com/android/compose-samples/blob/main/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Button.kt) from the Jetsnack sample. +If you've introduced new class types --- such as `List` to represent +gradients --- then it may be better to implement components from scratch instead +of wrapping them. For an example, take a look at +[`JetsnackButton`](https://github.com/android/compose-samples/blob/main/Jetsnack/app/src/main/java/com/example/jetsnack/ui/components/Button.kt) +from the Jetsnack sample. ## Recommended for you diff --git a/jetpack-compose/theming/styles/references/android/develop/ui/compose/styles/fundamentals.md b/jetpack-compose/theming/styles/references/android/develop/ui/compose/styles/fundamentals.md index 901315d..ab0d26d 100644 --- a/jetpack-compose/theming/styles/references/android/develop/ui/compose/styles/fundamentals.md +++ b/jetpack-compose/theming/styles/references/android/develop/ui/compose/styles/fundamentals.md @@ -6,7 +6,10 @@ There are three ways you can adopt Styles throughout your app: ## Available properties on Styles -Styles support many of the same properties that modifiers support; however, not everything that is a modifier can be replicated with a Style. You still need modifiers for certain behaviors, like interactions, custom drawing, or stacking of properties. +Styles support many of the same properties that modifiers support; however, not +everything that is a modifier can be replicated with a Style. You still need +modifiers for certain behaviors, like interactions, custom drawing, or stacking +of properties. | Grouping | Properties | Inherited by children | |---|---|---| @@ -33,7 +36,6 @@ Styles support many of the same properties that modifiers support; however, not Components that expose a `Style` parameter allow you to set their styling: -
```kotlin BaseButton( @@ -42,14 +44,13 @@ BaseButton( ) { BaseText("Click me") } - ```
-Within the style lambda, you can set various properties, such as `externalPadding` or `background`: +Within the style lambda, you can set various properties, such as `externalPadding` +or `background`: -
```kotlin BaseButton( @@ -58,18 +59,19 @@ BaseButton( ) { BaseText("Click me") } - ```
-For the full list of supported properties, see [Available properties on Styles](https://developer.android.com/develop/ui/compose/styles/fundamentals#properties-styles). +For the full list of supported properties, see [Available properties on +Styles](https://developer.android.com/develop/ui/compose/styles/fundamentals#properties-styles). ## Apply Styles using modifiers for components with no existing parameter -For components that lack a built-in style parameter, you can still apply styles with the `styleable` modifier. This approach is also useful when developing your own custom components. +For components that lack a built-in style parameter, you can still apply styles +with the `styleable` modifier. This approach is also useful when developing your +own custom components. -
```kotlin Row( @@ -77,14 +79,13 @@ Row( ) { BaseText("Content") } - ```
-Similar to the `style` parameter, you can include properties like `background`, `contentPadding`, or `externalPadding` inside the lambda. +Similar to the `style` parameter, you can include properties like `background`, +`contentPadding`, or `externalPadding` inside the lambda. -
```kotlin Row( @@ -94,7 +95,6 @@ Row( ) { BaseText("Content") } - ```
@@ -102,28 +102,35 @@ Row( > [!NOTE] > **Note:** When using `Modifier.styleable`, the child composables won't have those properties applied to them, unless they are inherited properties. Only the container with the `styleable` modifier has those properties applied. -Multiple chained `Modifier.styleable` modifiers are additive with non-inherited properties on the applied composable, behaving similarly to multiple modifiers defining the same properties. For inherited properties, these are overridden, and the last `styleable` modifier in the chain sets the values. +Multiple chained `Modifier.styleable` modifiers are additive with non-inherited +properties on the applied composable, behaving similarly to multiple modifiers +defining the same properties. For inherited properties, these are overridden, +and the last `styleable` modifier in the chain sets the values. -When using `Modifier.styleable`, you may also want to create and supply a `StyleState` to be used with the modifier to apply state-based styling. For more details, see [State and animations with Styles](https://developer.android.com/develop/ui/compose/styles/state-animations). +When using `Modifier.styleable`, you may also want to create and supply a +`StyleState` to be used with the modifier to apply state-based styling. For more +details, see [State and animations with +Styles](https://developer.android.com/develop/ui/compose/styles/state-animations). ## Define a standalone Style You can define a standalone Style for reusability purposes: -
```kotlin val style = Style { background(Color.Blue) } - ```
-You can then pass that defined style into a composable's style parameter or with `Modifier.styleable`. When using `Modifier.styleable`, you also need to create a `StyleState` object. `StyleState` is covered in detail in the [State and animations with Styles](https://developer.android.com/develop/ui/compose/styles/state-animations) documentation. +You can then pass that defined style into a composable's style parameter or with +`Modifier.styleable`. When using `Modifier.styleable`, you also need to create a +`StyleState` object. `StyleState` is covered in detail in the [State and +animations with Styles](https://developer.android.com/develop/ui/compose/styles/state-animations) documentation. -The following example shows how you can apply a Style either directly through a component's built-in parameters, or through a `Modifier.styleable`: +The following example shows how you can apply a Style either directly through a +component's built-in parameters, or through a `Modifier.styleable`: -
```kotlin val style = Style { background(Color.Blue) } @@ -140,14 +147,12 @@ Column( ) { BaseText("Column content") } - ```
You can also pass that Style into multiple components: -
```kotlin val style = Style { background(Color.Blue) } @@ -171,16 +176,15 @@ Row( ) { BaseText("Row") } - ```
## Add multiple Style properties -You can add multiple Style properties by setting different properties on each line: +You can add multiple Style properties by setting different properties on each +line: -
```kotlin BaseButton( @@ -192,7 +196,6 @@ BaseButton( ) { BaseText("Button") } - ```
@@ -200,9 +203,12 @@ BaseButton( > [!IMPORTANT] > **Important:** Unlike modifier-based styling, properties in Styles override one another; the last property defined takes precedence. -Properties in Styles are not additive, unlike modifier-based styling. Styles take the last set value in the list of properties within one style block. In the following example, with the background set twice, the `TealColor` is the applied background. For padding, `contentPaddingTop` overrides the top padding set by `contentPadding` and does not combine the values. +Properties in Styles are not additive, unlike modifier-based styling. Styles +take the last set value in the list of properties within one style block. In the +following example, with the background set twice, the `TealColor` is the applied +background. For padding, `contentPaddingTop` overrides the top +padding set by `contentPadding` and does not combine the values. -
```kotlin BaseButton( @@ -221,7 +227,6 @@ BaseButton( ) { BaseText("Click me!") } - ```
@@ -231,9 +236,9 @@ overrides](https://developer.android.com/static/develop/ui/compose/styles/images ## Merge multiple style objects -You can create multiple Style objects and pass them into the style parameter of your composable. +You can create multiple Style objects and pass them into the style parameter of +your composable. -
```kotlin val style1 = Style { background(TealColor) } @@ -247,7 +252,6 @@ BaseButton( ) { BaseText("Click me!") } - ```
@@ -255,9 +259,12 @@ BaseButton( ![Button with background color and contentPaddingTop set](https://developer.android.com/static/develop/ui/compose/styles/images/button_content_padding_top.png) **Figure 2.** Button with background color and `contentPaddingTop` set. -When multiple Styles specify the same property, the last set property is chosen. Because properties are not additive in Styles, the last padding passed in overrides the `contentPaddingHorizontal` set by the initial `contentPadding`. Additionally, the last background color overrides the background color set by the initial style passed in. +When multiple Styles specify the same property, the last set +property is chosen. Because properties are not additive in Styles, the last +padding passed in overrides the `contentPaddingHorizontal` set by the initial +`contentPadding`. Additionally, the last background color overrides the +background color set by the initial style passed in. -
```kotlin val style1 = Style { @@ -278,12 +285,12 @@ BaseButton( ) { BaseText("Click me!") } - ```
-In this case, the styling applied has a light gray background and `32.dp` padding, except for the left and right padding, which has a value of `8.dp`. +In this case, the styling applied has a light gray background and `32.dp` padding, +except for the left and right padding, which has a value of `8.dp`. ![Button with contentPadding that's overridden by different Styles](https://developer.android.com/static/develop/ui/compose/styles/images/button_content_padding_overrides.png) **Figure 3.** Button with `contentPadding` that's overridden by different Styles. @@ -292,7 +299,9 @@ Styles](https://developer.android.com/static/develop/ui/compose/styles/images/bu > [!NOTE] > **Note:** While the Style APIs are experimental, you need to opt-in to enable Style inheritance by setting the flag `ComposeFoundationFlags.isInheritedTextStyleEnabled = true`. -Certain style properties, such as `contentColor` and text style-related properties, propagate to the child composables. A style set on a child composable overrides the inherited parent styling for that specific child. +Certain style properties, such as `contentColor` and text style-related +properties, propagate to the child composables. A style set on a child +composable overrides the inherited parent styling for that specific child. ![Style propagation with Style, styleable, and direct parameters](https://developer.android.com/static/develop/ui/compose/styles/images/styles_modifiers_precedence_ordering.png) **Figure 4.** Style propagation with `Style`, `styleable`, and direct parameters. @@ -308,9 +317,9 @@ parameters](https://developer.android.com/static/develop/ui/compose/styles/image ### Parent styling -You can set text properties (such as `contentColor`) from the parent composable, and they propagate to all child `Text` composables. +You can set text properties (such as `contentColor`) from the parent composable, +and they propagate to all child `Text` composables. -
```kotlin val styleState = remember { MutableStyleState(null) } @@ -327,7 +336,6 @@ Column( BaseText("certain properties") BaseText("from their parents") } - ```
@@ -337,9 +345,10 @@ inheritance](https://developer.android.com/static/develop/ui/compose/styles/imag ### Child override of properties -You can also set styling on a specific `Text` composable. If the parent composable has styling set, the styling set on the child composable overrides the parent composable's styling. +You can also set styling on a specific `Text` composable. If the parent composable +has styling set, the styling set on the child composable overrides the +parent composable's styling. -
```kotlin val styleState = remember { MutableStyleState(null) } @@ -358,7 +367,6 @@ Column( BaseText("override properties") BaseText("set by their parents") } - ```
@@ -368,40 +376,39 @@ properties](https://developer.android.com/static/develop/ui/compose/styles/image ## Implement custom Style properties -You can create custom properties that map to existing Style definitions by using extension functions on the `StyleScope`, as shown in the following example: +You can create custom properties that map to existing Style definitions by using +extension functions on the `StyleScope`, as shown in the following example: -
```kotlin fun StyleScope.outlinedBackground(color: Color) { border(1.dp, color) background(color) } - ```
Apply this new property within a Style definition: -
```kotlin val customExtensionStyle = Style { outlinedBackground(Color.Blue) } - ```
-Creating new styleable properties is unsupported. If your use case requires such support, submit a [feature request](https://issuetracker.google.com/issues/new?component=612128). +Creating new styleable properties is unsupported. If your use case +requires such support, submit a [feature request](https://issuetracker.google.com/issues/new?component=612128). ## Read `CompositionLocal` values -It's a common pattern to store design system tokens within a `CompositionLocal`, to access the variables without needing to pass them as parameters. Styles can access `CompositionLocal`s to retrieve system-wide values within a style: +It's a common pattern to store design system tokens within a `CompositionLocal`, +to access the variables without needing to pass them as parameters. Styles +can access `CompositionLocal`s to retrieve system-wide values within a style: -
```kotlin val buttonStyle = Style { @@ -409,7 +416,6 @@ val buttonStyle = Style { shape(RoundedCornerShape(50)) background(Brush.verticalGradient(LocalCustomColors.currentValue.background)) } - ```
\ No newline at end of file diff --git a/jetpack-compose/theming/styles/references/android/develop/ui/compose/styles/state-animations.md b/jetpack-compose/theming/styles/references/android/develop/ui/compose/styles/state-animations.md index 8c387a6..01683bd 100644 --- a/jetpack-compose/theming/styles/references/android/develop/ui/compose/styles/state-animations.md +++ b/jetpack-compose/theming/styles/references/android/develop/ui/compose/styles/state-animations.md @@ -1,8 +1,15 @@
-The Styles API offers a declarative and streamlined approach to managing UI changes during interaction states like `hovered`, `focused`, and `pressed`. With this API, you can significantly decrease the boilerplate code typically required when using modifiers. +The Styles API offers a declarative and streamlined approach to managing UI +changes during interaction states like `hovered`, `focused`, and `pressed`. With +this API, you can significantly decrease the boilerplate code typically required +when using modifiers. -To facilitate reactive styling, `StyleState` acts as a stable, read-only interface that tracks the active state of an element (such as its enabled, pressed, or focused status). Within a `StyleScope`, you can access this through the `state` property to implement conditional logic directly in your Style definitions. +To facilitate reactive styling, `StyleState` acts as a stable, read-only +interface that tracks the active state of an element (such as its enabled, +pressed, or focused status). Within a `StyleScope`, you can access this through +the `state` property to implement conditional logic directly in your Style +definitions. ## State-based interaction: Hovered, focused, pressed, selected, enabled, toggled @@ -14,13 +21,15 @@ Styles come with built-in support for common interactions: - Enabled - Toggled -It's also possible to support custom states. See the [Custom State Styling with StyleState](https://developer.android.com/develop/ui/compose/styles/state-animations#custom-state) section for more information. +It's also possible to support custom states. See the [Custom State Styling with +StyleState](https://developer.android.com/develop/ui/compose/styles/state-animations#custom-state) section for more information. ### Handle interaction states with Style parameters -The following example demonstrates modifying the `background` and `borderColor` in response to interaction states, specifically switching to purple when hovered and blue when focused: +The following example demonstrates modifying the `background` and `borderColor` +in response to interaction states, specifically switching to purple when hovered +and blue when focused: -
```kotlin @Preview @@ -47,17 +56,16 @@ private fun OpenButton() { } ) } - - ```
**Figure 1.** Changing background color based on hovered and focused states. -You can also create nested state definitions. For example, you can define a specific style for when a button is being both pressed and hovered simultaneously: +You can also create nested state definitions. For example, you can define a +specific style for when a button is being both pressed and hovered +simultaneously: -
```kotlin @Composable @@ -91,7 +99,6 @@ private fun OpenButton_CombinedStates() { } ) } - ```
@@ -100,15 +107,18 @@ private fun OpenButton_CombinedStates() { ### Custom composables with Modifier.styleable -When creating your own `styleable` components, you must connect an `interactionSource` to a `styleState`. Then, pass this state into `Modifier.styleable` to utilize it. +When creating your own `styleable` components, you must connect an +`interactionSource` to a `styleState`. Then, pass this state into +`Modifier.styleable` to utilize it. -Consider a scenario where your design system includes a `GradientButton`. You may want to create a `LoginButton` that inherits from `GradientButton`, but alters its colors during interactions, like being pressed. +Consider a scenario where your design system includes a `GradientButton`. You +may want to create a `LoginButton` that inherits from `GradientButton`, but +alters its colors during interactions, like being pressed. - To enable `interactionSource` style updates, include an `interactionSource` as a parameter within your composable. Use the provided parameter or, if one is not supplied, initialize a new `MutableInteractionSource`. - Initialize the `styleState` by providing the `interactionSource`. Make sure the `styleState`'s enabled status reflects the value of the provided enabled parameter. - Assign the `interactionSource` to the `focusable` and `clickable` modifiers. Finally, apply the `styleState` to the modifier's `styleable` parameter. -
```kotlin @Composable @@ -137,14 +147,13 @@ private fun GradientButton( content = content, ) } - ```
-You can now use the `interactionSource` state to drive style modifications with the pressed, focused, and hovered options inside the style block: +You can now use the `interactionSource` state to drive style modifications with +the pressed, focused, and hovered options inside the style block: -
```kotlin @Preview @@ -165,7 +174,6 @@ fun LoginButton() { BaseText("Login") } } - ```
@@ -174,9 +182,12 @@ fun LoginButton() { ## Animate style changes -Styles state changes come with built-in animation support. You can wrap the new property within any state change block with `animate` to automatically add animations between different states. This is similar to the `animate*AsState` APIs. The following example animates the `borderColor` from black to blue when the state changes to focused: +Styles state changes come with built-in animation support. You can wrap the new +property within any state change block with `animate` to automatically add +animations between different states. This is similar to the `animate*AsState` +APIs. The following example animates the `borderColor` from black to blue when +the state changes to focused: -
```kotlin val animatingStyle = Style { @@ -211,16 +222,16 @@ private fun AnimatingStyleChanges() { } } - ```
**Figure 4.** Animating color changes on press. -The `animate` API accepts an `animationSpec` to change the duration or shape of the animation curve. The following example animates the size of the box with a `spring` spec: +The `animate` API accepts an `animationSpec` to change the duration or shape of +the animation curve. The following example animates the size of the box with a +`spring` spec: -
```kotlin val animatingStyleSpec = Style { @@ -256,7 +267,6 @@ fun AnimatingStyleChangesSpec() { ) .styleable(styleState, animatingStyleSpec)) } - ```
@@ -265,7 +275,11 @@ fun AnimatingStyleChangesSpec() { ## Custom state styling with StyleState -Depending on your composable use case, you may have different styles that are backed by custom states. For example, if you have a media app, you may want to have different styling for the buttons in your `MediaPlayer` composable depending on the playback state of the player. Follow these steps to create and use your own custom state: +Depending on your composable use case, you may have different styles that are +backed by custom states. For example, if you have a media app, you may want to +have different styling for the buttons in your `MediaPlayer` composable +depending on the playback state of the player. Follow these steps to create and +use your own custom state: 1. Define custom key 2. Create `StyleState` extension @@ -273,9 +287,11 @@ Depending on your composable use case, you may have different styles that are ba ### Define custom key -To create a custom state-based style, first create a [`StyleStateKey`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/style/StyleStateKey) and pass in the default state value. When the app launches, the media player is in the `Stopped` state, so it's initialized in this way: +To create a custom state-based style, first create a +[`StyleStateKey`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/style/StyleStateKey) and pass in the default state value. When the +app launches, the media player is in the `Stopped` state, so it's initialized in +this way: -
```kotlin enum class PlayerState { @@ -285,16 +301,16 @@ enum class PlayerState { } val playerStateKey = StyleStateKey(PlayerState.Stopped) - ```
### Create StyleState extension functions -Define an extension function on `StyleState` to query the current `playState`. Then, create extension functions on `StyleScope` with your custom states passing in the `playStateKey`, a lambda with the specific state, and the style. +Define an extension function on `StyleState` to query the current `playState`. +Then, create extension functions on `StyleScope` with your custom states passing +in the `playStateKey`, a lambda with the specific state, and the style. -
```kotlin // Extension Function on MutableStyleState to query and set the current playState @@ -308,16 +324,16 @@ fun StyleScope.playerPlaying(block: () -> Unit) { fun StyleScope.playerPaused(block: () -> Unit) { state(playerStateKey, block, { key, state -> state[key] == PlayerState.Paused }) } - ```
### Link to custom state -Define the `styleState` in your composable and set the `styleState.playState` equal to incoming state. Pass `styleState` into the `styleable` function on the modifier. +Define the `styleState` in your composable and set the `styleState.playState` +equal to incoming state. Pass `styleState` into the `styleable` function on the +modifier. -
```kotlin @Composable @@ -336,14 +352,13 @@ fun MediaPlayer( ///.. } } - ```
-Within the `style` lambda, you can apply state-based styling for custom states, using the previously defined extension functions. +Within the `style` lambda, you can apply state-based styling for custom states, +using the previously defined extension functions. -
```kotlin @Composable @@ -370,14 +385,12 @@ fun StyleStateKeySample() { style = style, state = PlayerState.Stopped) } - ```
The following code is the full snippet for this example: -
```kotlin enum class PlayerState { @@ -443,7 +456,6 @@ fun StyleStateKeySample() { style = style, state = PlayerState.Stopped) } - ```
\ No newline at end of file diff --git a/jetpack-compose/theming/styles/references/android/develop/ui/compose/styles/styles-vs-modifiers.md b/jetpack-compose/theming/styles/references/android/develop/ui/compose/styles/styles-vs-modifiers.md index e6a756b..cd9bc50 100644 --- a/jetpack-compose/theming/styles/references/android/develop/ui/compose/styles/styles-vs-modifiers.md +++ b/jetpack-compose/theming/styles/references/android/develop/ui/compose/styles/styles-vs-modifiers.md @@ -1,4 +1,7 @@ -Styles differ from modifiers by design. Styles don't replace modifiers; instead, the two systems coexist with different goals. Internally, a Style is a modifier. You can do everything Styles can do with modifiers, but not all functionality in modifiers is available in Styles. +Styles differ from modifiers by design. Styles don't replace modifiers; instead, +the two systems coexist with different goals. Internally, a Style is a modifier. +You can do everything Styles can do with modifiers, but not all functionality in +modifiers is available in Styles. **Important:** - **Choose Styles if:** You need to override a default of an existing component, perform high-performance animations, or define a theme-wide set of properties for a component. @@ -16,7 +19,8 @@ The following is a comparison between Styles versus modifiers: ## Limitations of modifiers -Modifiers have many benefits in the current Compose landscape. However, Styles address some limitations of modifiers, which the following list describes: +Modifiers have many benefits in the current Compose landscape. However, Styles +address some limitations of modifiers, which the following list describes: - Modifiers are typically created in the Composition phase. Updates can force a full rerun of Composition, Layout, and Draw, even for small visual changes like color, unless you create lambda-based modifiers. - Conditional modifiers require disruptive if-else logic within fluent chains. Animating them requires manual state boilerplate and lacks a high-performance "auto-animate" mechanism. @@ -25,15 +29,18 @@ Modifiers have many benefits in the current Compose landscape. However, Styles a ## Limitations of Styles -While Styles can fill in some of the gaps that modifiers have, they also have some limitations, which show how they cannot entirely replace modifiers: +While Styles can fill in some of the gaps that modifiers have, they also have +some limitations, which show how they cannot entirely replace modifiers: - Styles are specialized Modifiers. While a modifier can do anything a Style does, the reverse is not true. Consequently, Styles can supplement, but cannot replace, modifiers. - Styles are limited to visual configuration (backgrounds, padding, borders). They cannot handle behaviors like click logic, gesture detection, or accessibility semantics. -- Resolving a Style into its final state is *more expensive than applying a single modifier*. The system must generate a data structure containing all possible property values, and the lookup of inherited properties further complicates this. +- Resolving a Style into its final state is *more expensive than applying a + single modifier*. The system must generate a data structure containing all possible property values, and the lookup of inherited properties further complicates this. ## When to use Styles over modifiers -While the choice to use Styles is largely dependent on your app and use cases, the following guidance helps determine when to prefer a style over a modifier: +While the choice to use Styles is largely dependent on your app and use cases, +the following guidance helps determine when to prefer a style over a modifier: - **To achieve theme-wide consistency:** Styles are designed to be "lifted" into a global theme. Instead of passing repetitive Modifiers to every component, you can define a single Style in your theme to create a unified look across the entire app. - **When performing frequent animations:** Styles evaluate during the Layout and Draw phases, allowing properties like color or scale to animate while bypassing the Composition phase entirely. This significantly reduces performance overhead. Use a Style instead of a modifier when doing visual property animations. diff --git a/jetpack-compose/theming/styles/references/android/develop/ui/compose/styles/theming.md b/jetpack-compose/theming/styles/references/android/develop/ui/compose/styles/theming.md index e80dfb9..c8a373b 100644 --- a/jetpack-compose/theming/styles/references/android/develop/ui/compose/styles/theming.md +++ b/jetpack-compose/theming/styles/references/android/develop/ui/compose/styles/theming.md @@ -1,7 +1,8 @@ > [!NOTE] > **Note:** Styles are `@Experimental` and likely to change in upcoming releases, with Material support for Styles added in future releases. If you have any feedback, [file Styles issues](https://issuetracker.google.com/issues/new?component=612128). -There are several ways you can build out your apps using Styles. What you choose depends on where your app sits in relation to its adoption of Material Design: +There are several ways you can build out your apps using Styles. What you choose +depends on where your app sits in relation to its adoption of Material Design: 1. Fully custom design system, not using Material Design - **Recommendation**: Define component styles that consume values from the theme, and expose style parameters on design system components. @@ -10,9 +11,15 @@ There are several ways you can build out your apps using Styles. What you choose ## The Style layer -In the traditional Compose model, customization often relies heavily on overriding global tokens (colors and typography) provided by `MaterialTheme`, or wrapping and overriding properties of a design system composable where possible. Sometimes, there are properties within the Material layer that are not exposed through the subsystems or parameters, but are hardcoded defaults on the component itself. +In the traditional Compose model, customization often relies heavily on +overriding global tokens (colors and typography) provided by `MaterialTheme`, or +wrapping and overriding properties of a design system composable where possible. +Sometimes, there are properties within the Material layer that are not exposed +through the subsystems or parameters, but are hardcoded defaults on the +component itself. -With the Styles API, there's a new layer of abstraction that's a bridge between subsystems and components: **Styles**. +With the Styles API, there's a new layer of abstraction that's a bridge between +subsystems and components: **Styles**. | Layer | Responsibility | Example | |---|---|---| @@ -25,9 +32,11 @@ With the Styles API, there's a new layer of abstraction that's a bridge between ### Atomic versus monolithic Styles -With the Styles API, you can break down a Style into separate atomic styles. Instead of defining complex, component-specific styles like `baseButtonStyle`, you can also create small, single-purpose utility styles. These act as your "atoms". +With the Styles API, you can break down a Style into separate atomic styles. +Instead of defining complex, component-specific styles like `baseButtonStyle`, +you can also create small, single-purpose utility styles. These act as your +"atoms". -
```kotlin // Define single-purpose "atomic" styles @@ -60,18 +69,19 @@ val interactiveShadowAtomic = Style { } } } - - ```
#### Composition using "then" -One of the powerful features of the new Styles API is the `then` operator, which lets you merge multiple `Style` objects. This lets you build a component using atomic utility classes. +One of the powerful features of the new Styles API is the `then` operator, which +lets you merge multiple `Style` objects. This lets you build a component using +atomic utility classes. **Traditional (non-atomic)**: + ```kotlin // One large monolithic style val buttonStyle = Style { @@ -79,39 +89,49 @@ val buttonStyle = Style { shape(RoundedCornerShape(8.dp)) background(Color.Blue) } - ```
**Atomic refactor**: + ```kotlin // Combine atoms to create the final appearance val buttonStyle = paddingAtomic then roundedCornerShapeAtomic then primaryBackgroundAtomic then interactiveShadowAtomic - ```
## Adopt Styles in your design system -Consider the following options when adopting Styles within your design system, depending on where in the spectrum your design system lies. +Consider the following options when adopting Styles within your design system, +depending on where in the spectrum your design system lies. ### Custom design system with Styles -***Consider when**: You've been handed an extensive brand guide that is not based on Material Design, and you are not planning to use Material Design*. +***Consider when**: You've been handed an extensive brand guide that is not +based on Material Design, and you are not planning to use Material Design*. -***Strategy**: Implement a fully custom design system, and expose styles as part of the theme*. +***Strategy**: Implement a fully custom design system, and expose styles as part +of the theme*. -This option is the custom path if you don't use Material as your main design system language. You bypass `MaterialTheme` entirely for visual definitions and have created your [own custom theme already](https://developer.android.com/develop/ui/compose/designsystems/custom#implementing-fully-custom). You build a `CompanyTheme` that acts as a container for your Styles. +This option is the custom path if you don't use Material as your main design +system language. You bypass `MaterialTheme` entirely for visual definitions and +have created your [own custom theme already](https://developer.android.com/develop/ui/compose/designsystems/custom#implementing-fully-custom). You build a `CompanyTheme` that +acts as a container for your Styles. - **How it works** : Create a `CompanyTheme` object that holds `Style` objects for every component in your system. Your components (either wrappers around Material logic or custom `Box` or `Layout` implementations) consume these styles directly, and expose a `Style` parameter for consumers of your design system. - **The Style layer**: Styles are the primary definition of your design system. Tokens are named variables fed into these styles. This allows for deep customization, such as defining unique animations for state changes (for example, animating scale and color on press). -If you are building out your own [custom theme](https://developer.android.com/develop/ui/compose/designsystems/custom) without using Material, and want to adopt styles, add your list of styles to your Theme. This lets you access your base styles from anywhere in your project. +If you are building out your own [custom theme](https://developer.android.com/develop/ui/compose/designsystems/custom) without using Material, and +want to adopt styles, add your list of styles to your Theme. This lets you +access your base styles from anywhere in your project. + +1. Create a `Styles` class that stores the various styles in your application + and create the defaults. For example, in the Jetsnack app - the class is + named `JetsnackStyles`: -1. Create a `Styles` class that stores the various styles in your application and create the defaults. For example, in the Jetsnack app - the class is named `JetsnackStyles`: ```kotlin object JetsnackStyles{ @@ -134,12 +154,13 @@ If you are building out your own [custom theme](https://developer.android.com/de contentColor(colors.textPrimary) } } - ```
-2. Provide `Styles` as part of your overall theme, and expose helper extension functions on `StyleScope` to access the subsystems: +2. Provide `Styles` as part of your overall theme, and expose helper extension + functions on `StyleScope` to access the subsystems: + ```kotlin @Immutable @@ -194,13 +215,13 @@ If you are building out your own [custom theme](https://developer.android.com/de ) } } - ```
3. Access `JetsnackStyles` within your composable: + ```kotlin @Composable fun CustomButton(modifier: Modifier, @@ -224,9 +245,13 @@ If you are building out your own [custom theme](https://developer.android.com/de Text(text) } } - ```
-Beyond global theme adoption, there are alternative strategies for incorporating `Styles` into your apps. You can leverage `Styles` inline for specific call sites or use static definitions when full theming capabilities are unnecessary. `Styles` shouldn't be swapped conditionally unless the whole style is fundamentally different. You should prefer accessing dynamic tokens inside a visual definition rather than switching between distinct style objects. \ No newline at end of file +Beyond global theme adoption, there are alternative strategies for incorporating +`Styles` into your apps. You can leverage `Styles` inline for specific call +sites or use static definitions when full theming capabilities are unnecessary. +`Styles` shouldn't be swapped conditionally unless the whole style is +fundamentally different. You should prefer accessing dynamic tokens inside a +visual definition rather than switching between distinct style objects. \ No newline at end of file diff --git a/media/media3-cast-integration/SKILL.md b/media/media3-cast-integration/SKILL.md index e6fb809..cc32af0 100644 --- a/media/media3-cast-integration/SKILL.md +++ b/media/media3-cast-integration/SKILL.md @@ -8,7 +8,7 @@ description: Implements Google Cast support in Android apps using Jetpack Media3 license: Complete terms in LICENSE.txt metadata: author: Google LLC - last-updated: '2026-08-06' + last-updated: '2026-08-14' keywords: - Android - Media3 @@ -42,9 +42,10 @@ metadata: To complete this step, you **MUST** ensure the following: -- In the app-level build file, declare the `media3-cast` dependency version 1.9.0 or higher. +- In the app-level build file, declare the `media3-cast` dependency version + 1.9.0 or higher. - implementation("androidx.media3:media3-cast:1.10.1") + implementation("androidx.media3:media3-cast:1.11.0") - Ensure required Media3 dependencies are present: @@ -55,7 +56,9 @@ To complete this step, you **MUST** ensure the following: - Enforce the same versions across all Media3 dependencies. -- Use configurations in "Add build dependencies" section of [Getting started with CastPlayer](references/android/media/media3/cast/create-castplayer.md) as the source of truth. +- Use configurations in "Add build dependencies" section of [Getting started + with CastPlayer](references/android/media/media3/cast/create-castplayer.md) as the source of + truth. - **For apps without an existing Cast integration:** @@ -70,7 +73,8 @@ To complete this step, you **MUST** ensure the following: To complete this step, you **MUST** ensure the following: - Inside the manifest's `` tag, declare the Cast options provider. -- Use `DefaultCastOptionsProvider` by default. See the "OptionsProvider" section in [Getting started with CastPlayer](references/android/media/media3/cast/create-castplayer.md). +- Use `DefaultCastOptionsProvider` by default. See the "OptionsProvider" section in [Getting started with + CastPlayer](references/android/media/media3/cast/create-castplayer.md). - Declare a custom `OptionsProvider` only if explicitly requested. See [Customize CastOptions](references/android/media/media3/cast/customize-castoptions.md). - Ensure `INTERNET` permission is present. Don't add any unnecessary permissions. - **If Migrating from Legacy Cast SDK:** @@ -88,7 +92,8 @@ Before integrating Media3 Cast, an existing app follows one of two setups: To complete this step, you **MUST** ensure the following: - Inside the application's `MediaSessionService` (or `MediaLibraryService`) `onCreate()` method, initialize `ExoPlayer` and `CastPlayer`. -- Use `CastPlayer` by default unless `RemoteCastPlayer` is explicitly requested. See the "Build a CastPlayer" section in [Getting started with CastPlayer](references/android/media/media3/cast/create-castplayer.md). +- Use `CastPlayer` by default unless `RemoteCastPlayer` is explicitly requested. See the "Build a CastPlayer" section in [Getting started with + CastPlayer](references/android/media/media3/cast/create-castplayer.md). - For `CastPlayer`, pass the instance directly to `MediaSession.Builder`. - Replace all legacy forwarding player wrappers. - Don't delete legacy class files yet to prevent compilation errors during migration. @@ -151,8 +156,10 @@ To complete this step, you **MUST** ensure the following: - Use the [`MediaRouteButton` composable](https://developer.android.com/reference/kotlin/androidx/media3/cast/MediaRouteButton.composable) from `androidx.media3.cast` package. - Don't use `AndroidView` in the Compose UI hierarchy. - Place `MediaRouteButton` in an area next to playback controls. Don't hide it behind system UI. -- Don't use `PlayerSurface` for custom player UI. Use the Material3 [`Player` composable](https://developer.android.com/reference/kotlin/androidx/media3/ui/compose/material3/Player.composable). -- Force recomposition on playback location shifts to ensure UI sync. Use key constraints on `DeviceInfo` changes: +- Don't use `PlayerSurface` for custom player UI. Use the Material3 [`Player` + composable](https://developer.android.com/reference/kotlin/androidx/media3/ui/compose/material3/Player.composable). +- Force recomposition on playback location shifts to ensure UI sync. Use key + constraints on `DeviceInfo` changes: @OptIn(UnstableApi::class) @Composable @@ -188,11 +195,13 @@ To complete this step, you **MUST** ensure the following: To complete this step, you **MUST** ensure the following: -- For View-based UI setups, see the "Add UI elements" section in [Getting started with CastPlayer](references/android/media/media3/cast/create-castplayer.md). +- For View-based UI setups, see the "Add UI elements" section in [Getting + started with CastPlayer](references/android/media/media3/cast/create-castplayer.md). - Casting Activities must extend `AppCompatActivity` or `FragmentActivity` and use a `Theme.AppCompat` descendant. - Ensure the `AppCompat` theme has a visible `ActionBar` if adding `MediaRouteButton` to the options menu. - Replace all instances and imports of `CastButtonFactory` with `MediaRouteButtonFactory`. -- Rebind `PlayerView.player` references upon `onDeviceInfoChanged` events to prevent black screens or UI freezes: +- Rebind `PlayerView.player` references upon `onDeviceInfoChanged` events to + prevent black screens or UI freezes: private val playerListener: Player.Listener = object : Player.Listener { diff --git a/media/media3-cast-integration/references/android/media/media3/cast/create-castplayer.md b/media/media3-cast-integration/references/android/media/media3/cast/create-castplayer.md index 9edd7bb..e8bd139 100644 --- a/media/media3-cast-integration/references/android/media/media3/cast/create-castplayer.md +++ b/media/media3-cast-integration/references/android/media/media3/cast/create-castplayer.md @@ -1,36 +1,48 @@ -The [`CastPlayer`](https://developer.android.com/reference/androidx/media3/cast/CastPlayer) is a Jetpack Media3 [Player](https://developer.android.com/reference/kotlin/androidx/media3/common/Player) implementation that supports both local playback and casting to a remote Cast-enabled device. [`CastPlayer`](https://developer.android.com/reference/androidx/media3/cast/CastPlayer) simplifies adding cast functionality to your app and provides rich features to seamlessly switch between local and remote playback. This guide shows you how to integrate [`CastPlayer`](https://developer.android.com/reference/androidx/media3/cast/CastPlayer) into your media app. +The [`CastPlayer`](https://developer.android.com/reference/androidx/media3/cast/CastPlayer) is a Jetpack Media3 [Player](https://developer.android.com/reference/kotlin/androidx/media3/common/Player) implementation that supports +both local playback and casting to a remote Cast-enabled device. [`CastPlayer`](https://developer.android.com/reference/androidx/media3/cast/CastPlayer) +simplifies adding cast functionality to your app and provides rich features to +seamlessly switch between local and remote playback. This guide shows you how to +integrate [`CastPlayer`](https://developer.android.com/reference/androidx/media3/cast/CastPlayer) into your media app. To integrate Cast with other platforms, see the [Cast SDK](https://developers.google.com/cast/docs/developers). ## Get a Cast-enabled device -To test `CastPlayer`, you need a [Cast-enabled device](https://store.google.com/gb/category/connected_home?hl=en-GB). Choices include Android TV, Chromecast, smart speakers, and smart displays. Verify that your device is set up and connected to the same Wi-Fi network as your development mobile for discovery. +To test `CastPlayer`, you need a [Cast-enabled device](https://store.google.com/gb/category/connected_home?hl=en-GB). Choices include Android +TV, Chromecast, smart speakers, and smart displays. Verify that your device is +set up and connected to the same Wi-Fi network as your development mobile for +discovery. ## Add build dependencies -To start using `CastPlayer`, add the AndroidX Media3 and `CastPlayer` dependencies to the `build.gradle` file of your app module. +To start using `CastPlayer`, add the AndroidX Media3 and `CastPlayer` +dependencies to the `build.gradle` file of your app module. ### Kotlin - implementation("androidx.media3:media3-exoplayer:1.10.1") - implementation("androidx.media3:media3-ui:1.10.1") - implementation("androidx.media3:media3-session:1.10.1") - implementation("androidx.media3:media3-cast:1.10.1") + implementation("androidx.media3:media3-exoplayer:1.11.0") + implementation("androidx.media3:media3-ui:1.11.0") + implementation("androidx.media3:media3-session:1.11.0") + implementation("androidx.media3:media3-cast:1.11.0") ### Groovy - implementation "androidx.media3:media3-exoplayer:1.10.1" - implementation "androidx.media3:media3-ui:1.10.1" - implementation "androidx.media3:media3-session:1.10.1" - implementation "androidx.media3:media3-cast:1.10.1" + implementation "androidx.media3:media3-exoplayer:1.11.0" + implementation "androidx.media3:media3-ui:1.11.0" + implementation "androidx.media3:media3-session:1.11.0" + implementation "androidx.media3:media3-cast:1.11.0" ## Configure your CastPlayer -To configure the [`CastPlayer`](https://developer.android.com/reference/androidx/media3/cast/CastPlayer), update your `AndroidManifest.xml` file with an options provider. +To configure the [`CastPlayer`](https://developer.android.com/reference/androidx/media3/cast/CastPlayer), update your `AndroidManifest.xml` file with an +options provider. ### Options provider -The [`CastPlayer`](https://developer.android.com/reference/androidx/media3/cast/CastPlayer) requires an options provider to configure its behavior. For a basic setup, you can use the [`DefaultCastOptionsProvider`](https://developer.android.com/reference/androidx/media3/cast/DefaultCastOptionsProvider) by adding it to your `AndroidManifest.xml` file. This uses default settings, including the default receiver application. +The [`CastPlayer`](https://developer.android.com/reference/androidx/media3/cast/CastPlayer) requires an options provider to configure its behavior. For a +basic setup, you can use the [`DefaultCastOptionsProvider`](https://developer.android.com/reference/androidx/media3/cast/DefaultCastOptionsProvider) by adding it to your +`AndroidManifest.xml` file. This uses default settings, including the default +receiver application. ... @@ -40,11 +52,15 @@ The [`CastPlayer`](https://developer.android.com/reference/androidx/media3/cast/ ... -To customize the configuration, implement your own custom `OptionsProvider`. See the [CastOptions](https://developer.android.com/media/media3/cast/customize-castoptions) guide to learn how. +To customize the configuration, implement your own custom `OptionsProvider`. See +the [CastOptions](https://developer.android.com/media/media3/cast/customize-castoptions) guide to learn how. ### Add a receiver for media transfers -Adding a `MediaTransferReceiver` to your manifest enables the System UI to discover Cast-enabled devices on the network and reroute media without opening the app activity. For example, a user can change the device playing your app's media from the [media notification](https://developer.android.com/media/implement/surfaces/mobile). +Adding a `MediaTransferReceiver` to your manifest enables the System UI to +discover Cast-enabled devices on the network and reroute media without opening +the app activity. For example, a user can change the device playing your app's +media from the [media notification](https://developer.android.com/media/implement/surfaces/mobile). ... @@ -54,10 +70,19 @@ Adding a `MediaTransferReceiver` to your manifest enables the System UI to disco ## Build a CastPlayer -For remote playback with Cast, your app should be able to manage playback even when the user isn't interacting with an Activity from your app, such as through the system media notification. For this reason, you should create your `ExoPlayer` (for local playback) and `CastPlayer` (for remote playback) instances in a service, such as [MediaSessionService](https://developer.android.com/media/media3/session/background-playback#service-lifecycle) or [MediaLibraryService](https://developer.android.com/guide/topics/media/session/medialibraryservice). First, create your `ExoPlayer` instance and then when building your [`CastPlayer`](https://developer.android.com/reference/androidx/media3/cast/CastPlayer) instance, set `ExoPlayer` as the local player instance. You can then switch media playback between your mobile and the Cast-enabled device from the media notification or the lock screen notification. Media3 uses the **Output Switcher** feature to handle player transfers when the output route changes from local to remote or from remote to local. +For remote playback with Cast, your app should be able to manage playback even +when the user isn't interacting with an Activity from your app, such as through +the system media notification. For this reason, you should create your +`ExoPlayer` (for local playback) and `CastPlayer` (for remote playback) +instances in a service, such as [MediaSessionService](https://developer.android.com/media/media3/session/background-playback#service-lifecycle) or [MediaLibraryService](https://developer.android.com/guide/topics/media/session/medialibraryservice). +First, create your `ExoPlayer` instance and then when building your +[`CastPlayer`](https://developer.android.com/reference/androidx/media3/cast/CastPlayer) instance, set `ExoPlayer` as the local player instance. You can +then switch media playback between your mobile and the Cast-enabled device from +the media notification or the lock screen notification. Media3 uses the **Output +Switcher** feature to handle player transfers when the output route changes from +local to remote or from remote to local. ![Screenshot showing the Output Switcher UI in notifications.](https://developer.android.com/static/media/media3/cast/images/output_switcher.jpeg) Figure 1: (a) Device chip on Media notification (b) Cast-enabled devices shown on tapping the device chip (c) Device chip on Lock screen notification -
### Kotlin @@ -70,8 +95,6 @@ override fun onCreate() { mediaSession = MediaSession.Builder(context, castPlayer).build() } - - ``` ### Java @@ -87,8 +110,6 @@ public void onCreate() { mediaSession = new MediaSession.Builder(/* context= */ context, /* player= */ castPlayer).build(); } - - ```
@@ -98,11 +119,17 @@ public void onCreate() { ## Add UI elements -Add a [`MediaRouteButton`](https://developer.android.com/reference/androidx/mediarouter/app/MediaRouteButton) to your app's UI. Tapping the [`MediaRouteButton`](https://developer.android.com/reference/androidx/mediarouter/app/MediaRouteButton) opens a dialog displaying a list of available Cast-enabled devices on the network. When the user selects a device, the media playback is transferred from the mobile to the selected receiver device. This section shows you how to add the button and listen for events to update your UI when playback switches between local and remote devices. +Add a [`MediaRouteButton`](https://developer.android.com/reference/androidx/mediarouter/app/MediaRouteButton) to your app's UI. Tapping the [`MediaRouteButton`](https://developer.android.com/reference/androidx/mediarouter/app/MediaRouteButton) +opens a dialog displaying a list of available Cast-enabled devices on the +network. When the user selects a device, the media playback is transferred from +the mobile to the selected receiver device. This section shows you how to add +the button and listen for events to update your UI when playback switches +between local and remote devices. ### Set the MediaRouteButton -There are four ways to add the [`MediaRouteButton`](https://developer.android.com/reference/androidx/mediarouter/app/MediaRouteButton) to your activity's UI. The best choice depends on your app's design and requirements. +There are four ways to add the [`MediaRouteButton`](https://developer.android.com/reference/androidx/mediarouter/app/MediaRouteButton) to your activity's UI. The +best choice depends on your app's design and requirements. - **Compose UI**: Add a button composable. - **Views UI** : @@ -120,9 +147,9 @@ There are four ways to add the [`MediaRouteButton`](https://developer.android.co #### Add a Composable `MediaRouteButton` to the Player -You can add the [`MediaRouteButton`](https://developer.android.com/reference/androidx/mediarouter/app/MediaRouteButton) Composable to your player's UI. For more information, see the [Compose](https://developer.android.com/media/media3/ui/compose) guide. +You can add the [`MediaRouteButton`](https://developer.android.com/reference/androidx/mediarouter/app/MediaRouteButton) Composable to your player's UI. For more +information, see the [Compose](https://developer.android.com/media/media3/ui/compose) guide. -
```kotlin @Composable @@ -147,17 +174,17 @@ fun PlayerComposeView(player: Player, modifier: Modifier = Modifier) { fun PrimaryControls(player: Player, modifier: Modifier = Modifier) { // ... } - - ```
#### Add the `MediaRouteButton` to the PlayerView -You can add the [`MediaRouteButton`](https://developer.android.com/reference/androidx/mediarouter/app/MediaRouteButton) directly within the [PlayerView](https://developer.android.com/guide/topics/media/ui/playerview)'s UI controls. After setting the [MediaController](https://developer.android.com/guide/topics/media/session/mediacontroller) as the player for your `PlayerView`, provide a `MediaRouteButtonViewProvider` to display the Cast button on the Player. +You can add the [`MediaRouteButton`](https://developer.android.com/reference/androidx/mediarouter/app/MediaRouteButton) directly within the [PlayerView](https://developer.android.com/guide/topics/media/ui/playerview)'s UI +controls. After setting the [MediaController](https://developer.android.com/guide/topics/media/session/mediacontroller) as the player for your +`PlayerView`, provide a `MediaRouteButtonViewProvider` to display the Cast +button on the Player. -
### Kotlin @@ -168,7 +195,6 @@ override fun onStart() { playerView.player = mediaController playerView.setMediaRouteButtonViewProvider(MediaRouteButtonViewProvider()) } - ``` ### Java @@ -181,14 +207,14 @@ public void onStart() { playerView.setPlayer(mediaController); playerView.setMediaRouteButtonViewProvider(new MediaRouteButtonViewProvider()); } - ```
#### Add the `MediaRouteButton` to the app bar menu -To set up a [`MediaRouteButton`](https://developer.android.com/reference/androidx/mediarouter/app/MediaRouteButton) in the app bar menu, create an XML menu and override `onCreateOptionsMenu` in your `Activity`. +To set up a [`MediaRouteButton`](https://developer.android.com/reference/androidx/mediarouter/app/MediaRouteButton) in the app bar menu, create an XML menu and +override `onCreateOptionsMenu` in your `Activity`. @@ -198,7 +224,6 @@ To set up a [`MediaRouteButton`](https://developer.android.com/reference/android app:actionProviderClass="androidx.mediarouter.app.MediaRouteActionProvider"/> -
### Kotlin @@ -224,7 +249,6 @@ override fun onCreateOptionsMenu(menu: Menu): Boolean { // ... return true } - ``` ### Java @@ -253,7 +277,6 @@ public boolean onCreateOptionsMenu(Menu menu) { // ... return true; } - ```
@@ -268,9 +291,9 @@ You can set up a [`MediaRouteButton`](https://developer.android.com/reference/an android:layout_height="wrap_content" app:mediaRouteButtonTint="@android:color/white" /> -To complete the setup for the [`MediaRouteButton`](https://developer.android.com/reference/androidx/mediarouter/app/MediaRouteButton), use the Media3 Cast `MediaRouteButtonFactory` in your `Activity` code. +To complete the setup for the [`MediaRouteButton`](https://developer.android.com/reference/androidx/mediarouter/app/MediaRouteButton), use the Media3 Cast +`MediaRouteButtonFactory` in your `Activity` code. -
### Kotlin @@ -282,7 +305,6 @@ override fun onCreate(savedInstanceState: Bundle?) { val unused = MediaRouteButtonFactory.setUpMediaRouteButton(context, it) } } - ``` ### Java @@ -296,16 +318,18 @@ public void onCreate(Bundle savedInstanceState) { ListenableFuture setUpFuture = MediaRouteButtonFactory.setUpMediaRouteButton(context, button); } - ```
### Activity Listener -Create a `Player.Listener` in your `Activity` to listen for changes to media playback location. When the `playbackType` changes between `PLAYBACK_TYPE_LOCAL` and `PLAYBACK_TYPE_REMOTE`, you can adjust your UI as needed. To prevent memory leaks and to confine listener activity to only when your app is visible, register the listener in `onStart` and unregister it in `onStop`: +Create a `Player.Listener` in your `Activity` to listen for changes to media +playback location. When the `playbackType` changes between `PLAYBACK_TYPE_LOCAL` +and `PLAYBACK_TYPE_REMOTE`, you can adjust your UI as needed. To prevent memory +leaks and to confine listener activity to only when your app is visible, +register the listener in `onStart` and unregister it in `onStop`: -
### Kotlin @@ -330,7 +354,6 @@ override fun onStop() { super.onStop() mediaController.removeListener(playerListener) } - ``` ### Java @@ -359,9 +382,9 @@ protected void onStop() { super.onStop(); mediaController.removeListener(playerListener); } - ```
-For more information about listening and responding to playback events, see the [player events](https://developer.android.com/media/media3/exoplayer/listening-to-player-events) guide. \ No newline at end of file +For more information about listening and responding to playback events, see the +[player events](https://developer.android.com/media/media3/exoplayer/listening-to-player-events) guide. \ No newline at end of file diff --git a/media/media3-cast-integration/references/android/media/media3/cast/customize-castoptions.md b/media/media3-cast-integration/references/android/media/media3/cast/customize-castoptions.md index caf237a..e5e5b3e 100644 --- a/media/media3-cast-integration/references/android/media/media3/cast/customize-castoptions.md +++ b/media/media3-cast-integration/references/android/media/media3/cast/customize-castoptions.md @@ -1,8 +1,11 @@ -To configure your app's Cast session, provide an [`OptionsProvider`](https://developers.google.com/android/reference/com/google/android/gms/cast/framework/OptionsProvider). Use the [`CastOptions`](https://developers.google.com/android/reference/com/google/android/gms/cast/framework/CastOptions) object built by the provider to set the receiver application ID, manage session lifecycles, and customize media playback behavior. +To configure your app's Cast session, provide an [`OptionsProvider`](https://developers.google.com/android/reference/com/google/android/gms/cast/framework/OptionsProvider). Use the +[`CastOptions`](https://developers.google.com/android/reference/com/google/android/gms/cast/framework/CastOptions) object built by the provider to set the receiver application ID, +manage session lifecycles, and customize media playback behavior. ## Use the default options provider -For a basic setup that uses the default Cast receiver application, add the `DefaultCastOptionsProvider` to your app's `AndroidManifest.xml` file: +For a basic setup that uses the default Cast receiver application, add the +`DefaultCastOptionsProvider` to your app's `AndroidManifest.xml` file: ... @@ -14,11 +17,13 @@ For a basic setup that uses the default Cast receiver application, add the `Defa ## Create a custom options provider -For more advanced configurations, such as setting a custom receiver application ID, you need to create your own `OptionsProvider`. +For more advanced configurations, such as setting a custom receiver application +ID, you need to create your own `OptionsProvider`. ### 1. Declare the provider in your manifest -First, declare your custom provider in `AndroidManifest.xml`. Make sure to use the fully qualified class name. +First, declare your custom provider in `AndroidManifest.xml`. Make sure to use +the fully qualified class name. ... @@ -30,11 +35,13 @@ First, declare your custom provider in `AndroidManifest.xml`. Make sure to use t ### 2. Implement the OptionsProvider interface -Next, create a class that implements the `OptionsProvider` interface. In this class, you must override `getCastOptions()` to return a `CastOptions` instance. The custom `OptionsProvider` class is where you configure your Cast session, for example, by setting your custom receiver application ID. +Next, create a class that implements the `OptionsProvider` interface. In this +class, you must override `getCastOptions()` to return a `CastOptions` instance. +The custom `OptionsProvider` class is where you configure your Cast session, for +example, by setting your custom receiver application ID. For more information, see [CastOptions.Builder](https://developers.google.com/android/reference/com/google/android/gms/cast/framework/CastOptions.Builder). -
### Kotlin @@ -57,7 +64,6 @@ class MyCustomCastOptionsProvider : OptionsProvider { private const val APP_ID = "" } } - ``` ### Java @@ -82,7 +88,6 @@ public static final class MyCustomCastOptionsProvider implements OptionsProvider return null; } } - ```
\ No newline at end of file diff --git a/media/media3-cast-integration/references/android/media/media3/cast/index.md b/media/media3-cast-integration/references/android/media/media3/cast/index.md index 1a38415..801a0e9 100644 --- a/media/media3-cast-integration/references/android/media/media3/cast/index.md +++ b/media/media3-cast-integration/references/android/media/media3/cast/index.md @@ -1,13 +1,21 @@ -The Media3 [`cast`](https://developer.android.com/reference/androidx/media3/cast/package-summary) module extends your media playback experience to Cast devices. It lets your app cast content to [compatible devices](https://developer.android.com/media/media3/cast/create-castplayer#get-device), such as TVs and speakers. The [sender](https://developers.google.com/cast/glossary#sender) app acts as a remote controller for media playback on the [receiver](https://developers.google.com/cast/glossary#receiver) device. This module integrates with the [Jetpack Media3](https://developer.android.com/guide/topics/media/media3) library, so you can use the same `Player` interface and UI components for both local and remote playback. +The Media3 [`cast`](https://developer.android.com/reference/androidx/media3/cast/package-summary) module extends your media playback experience to +Cast devices. It lets your app cast content to [compatible devices](https://developer.android.com/media/media3/cast/create-castplayer#get-device), +such as TVs and speakers. The [sender](https://developers.google.com/cast/glossary#sender) app acts as a remote controller for media +playback on the [receiver](https://developers.google.com/cast/glossary#receiver) device. This module integrates with the [Jetpack +Media3](https://developer.android.com/guide/topics/media/media3) library, so you can use the same `Player` interface and UI +components for both local and remote playback. ## Media3 CastPlayer -The `CastPlayer` is a media player API included in Media3 that supports both local and remote media playback. It implements the `Player` interface to manage playback, simplifying the integration of casting into your media experience. +The `CastPlayer` is a media player API included in Media3 that supports both +local and remote media playback. It implements the `Player` interface to manage +playback, simplifying the integration of casting into your media experience. [Go to Media3 CastPlayer](https://developer.android.com/media/media3/cast/create-castplayer) ## Other Cast integrations -The Cast SDK overview explains how to integrate Cast with other platforms, such as building a receiver app or integrating with the web sender SDK. +The Cast SDK overview explains how to integrate Cast with other platforms, such +as building a receiver app or integrating with the web sender SDK. [Explore Cast SDK](https://developers.google.com/cast/docs/developers) \ No newline at end of file diff --git a/navigation/navigation-3/SKILL.md b/navigation/navigation-3/SKILL.md index a7029fe..a19f8ba 100644 --- a/navigation/navigation-3/SKILL.md +++ b/navigation/navigation-3/SKILL.md @@ -8,7 +8,7 @@ description: Learn how to install and migrate to Jetpack Navigation 3, and how t license: Complete terms in LICENSE.txt metadata: author: Google LLC - last-updated: '2026-08-06' + last-updated: '2026-08-14' keywords: - recipe - Android diff --git a/navigation/navigation-3/references/android/guide/navigation/navigation-3/index.md b/navigation/navigation-3/references/android/guide/navigation/navigation-3/index.md index 66f2297..d7df2d8 100644 --- a/navigation/navigation-3/references/android/guide/navigation/navigation-3/index.md +++ b/navigation/navigation-3/references/android/guide/navigation/navigation-3/index.md @@ -1,5 +1,3 @@ -
- ## Android skills [View on GitHub](https://github.com/android/skills/tree/main/navigation/navigation-3) @@ -12,7 +10,10 @@ Use an Android skill to help you build using Jetpack Navigation 3. To install th
-Navigation 3 is a new navigation library designed to work with Compose. With Navigation 3, you have full control over your back stack, and navigating to and from destinations is as simple as adding and removing items from a list. It creates a flexible app navigation system by providing: +Navigation 3 is a new navigation library designed to work with Compose. With +Navigation 3, you have full control over your back stack, and navigating to and +from destinations is as simple as adding and removing items from a list. It +creates a flexible app navigation system by providing: - Conventions for modeling a back stack, where each entry on the back stack represents content that the user has navigated to - A UI that automatically updates with back stack changes (including animations) @@ -22,23 +23,28 @@ Navigation 3 is a new navigation library designed to work with Compose. With Nav At a high level, you implement Navigation 3 in the following ways: -1. Define the content that users can navigate to in your app, each with a unique key, and add a function to resolve that key to the content. See [Resolve keys to content](https://developer.android.com/guide/navigation/navigation-3/basics#resolve-keys). +1. Define the content that users can navigate to in your app, each with a unique key, and add a function to resolve that key to the content. See [Resolve keys + to content](https://developer.android.com/guide/navigation/navigation-3/basics#resolve-keys). 2. Create a back stack that keys are pushed onto and removed as users navigate your app. See [Create a back stack](https://developer.android.com/guide/navigation/navigation-3/basics#create-back). -3. Use a [`NavDisplay`](https://developer.android.com/reference/kotlin/androidx/navigation3/ui/NavDisplay.composable) to display your app's back stack. Whenever the back stack changes, it updates the UI to display relevant content. See [Display the back stack](https://developer.android.com/guide/navigation/navigation-3/basics#display-back). +3. Use a [`NavDisplay`](https://developer.android.com/reference/kotlin/androidx/navigation3/ui/NavDisplay.composable) to display your app's back stack. Whenever the back stack changes, it updates the UI to display relevant content. See [Display + the back stack](https://developer.android.com/guide/navigation/navigation-3/basics#display-back). 4. Modify `NavDisplay`'s [scene strategies](https://developer.android.com/guide/navigation/navigation-3/custom-layouts) as needed to support adaptive layouts and different platforms. You can see the [full source code](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:navigation3/) for Navigation 3 on AOSP. ## Improvements upon Jetpack Navigation -Navigation 3 improves upon the original Jetpack Navigation API in the following ways: +Navigation 3 improves upon the original Jetpack Navigation API in the following +ways: - Provides a simpler integration with Compose - Offers you full control of the back stack - Makes it possible to create layouts that can read more than one destination from the back stack at the same time, allowing them to adapt to changes in window size and other inputs. -Read more about Navigation 3's principles and API design choices in [this blog post](https://android-developers.googleblog.com/2025/05/announcing-jetpack-navigation-3-for-compose.html). +Read more about Navigation 3's principles and API design choices in [this blog +post](https://android-developers.googleblog.com/2025/05/announcing-jetpack-navigation-3-for-compose.html). ## Code samples -The [recipes repository](https://github.com/android/nav3-recipes) contains examples of how to use the Navigation 3 building blocks to solve common navigation challenges. \ No newline at end of file +The [recipes repository](https://github.com/android/nav3-recipes) contains examples of how to use the +Navigation 3 building blocks to solve common navigation challenges. \ No newline at end of file diff --git a/navigation/navigation-3/references/android/guide/navigation/navigation-3/migration-guide.md b/navigation/navigation-3/references/android/guide/navigation/navigation-3/migration-guide.md index 970e3da..23a5907 100644 --- a/navigation/navigation-3/references/android/guide/navigation/navigation-3/migration-guide.md +++ b/navigation/navigation-3/references/android/guide/navigation/navigation-3/migration-guide.md @@ -1,5 +1,3 @@ -
- ## Android skills [View on GitHub](https://github.com/android/skills/tree/main/navigation/navigation-3) @@ -24,14 +22,18 @@ To migrate your app from [Navigation 2](https://developer.android.com/guide/navi ## Preparation -The following sections describe the prerequisites for migration and assumptions about your project. They also cover the features that are supported for migration, and those that aren't. +The following sections describe the prerequisites for migration and assumptions +about your project. They also cover the features that are supported for +migration, and those that aren't. ### Prerequisites - You must use a `compileSdk` of 36 or later. - You should be familiar with [navigation terminology](https://developer.android.com/guide/navigation). -- Destinations are composable functions. Navigation 3 is designed exclusively for Compose. To use Fragments and Views in Compose, see [Using Views in Compose](https://developer.android.com/develop/ui/compose/migrate/interoperability-apis/views-in-compose). -- Routes are strongly typed. If you use string-based routes, [migrate to type-safe routes](https://medium.com/androiddevelopers/type-safe-navigation-for-compose-105325a97657) first ([example](https://github.com/android/nowinandroid/pull/1413)). +- Destinations are composable functions. Navigation 3 is designed exclusively for Compose. To use Fragments and Views in Compose, see [Using Views in + Compose](https://developer.android.com/develop/ui/compose/migrate/interoperability-apis/views-in-compose). +- Routes are strongly typed. If you use string-based routes, [migrate to + type-safe routes](https://medium.com/androiddevelopers/type-safe-navigation-for-compose-105325a97657) first ([example](https://github.com/android/nowinandroid/pull/1413)). - *Optional (but highly recommended)*: Test coverage that verifies existing navigation behavior. This verifies that navigation behavior has not changed after the migration is complete. ### Assumptions @@ -43,7 +45,9 @@ This guide makes the following assumptions about you and your project: - You always exit the app through the **Home** screen, which is the first screen displayed when the app launches. - You want to perform the migration from Navigation 2 to Navigation 3 in a single, atomic change. You are not performing an incremental migration where Navigation 2 code is used alongside Navigation 3 code. -**AI Agent**: Before changing any code, check if these assumptions about the user's project are true. If not, stop the migration and ask the user how to proceed. +**AI Agent**: Before changing any code, check if these assumptions about the +user's project are true. If not, stop the migration and ask the user how to +proceed. ### Supported features @@ -54,39 +58,49 @@ This guide lets you migrate the following features: ### Features supported through recipes -The [code recipes repository](https://github.com/android/nav3-recipes) demonstrates more nuanced use cases, including: +The [code recipes repository](https://github.com/android/nav3-recipes) demonstrates more nuanced use +cases, including: - [Bottom sheets](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/bottomsheet) (instructions are provided in this guide) - [Modularized navigation code and injected destinations](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/modular/hilt) - [Using and passing arguments to `ViewModel`](https://github.com/android/nav3-recipes?tab=readme-ov-file#passing-navigation-arguments-to-viewmodels) - [Returning results from a screen](https://github.com/android/nav3-recipes?tab=readme-ov-file#returning-results) -If your project has any of these features, check the relevant recipe to understand how to implement them. +If your project has any of these features, check the relevant recipe to +understand how to implement them. -**AI Agent**: Before changing any code, check if the project contains any features supported through recipes. If it does, check the recipe's README and source code. Create a migration plan based on the recipe. Do not proceed without confirming the plan with the user. +**AI Agent**: Before changing any code, check if the project contains any +features supported through recipes. If it does, check the recipe's README and +source code. Create a migration plan based on the recipe. Do not proceed without +confirming the plan with the user. ### Unsupported features -This migration guide and the code recipes don't yet support the following features. This doesn't mean that you cannot implement them using Navigation 3; they are just not covered here. +This migration guide and the code recipes don't yet support the following +features. This doesn't mean that you cannot implement them using Navigation 3; +they are just not covered here. - More than one level of nested navigation - Shared destinations: screens that can move between different back stacks - [Custom destination types](https://developer.android.com/guide/navigation/design/kotlin-dsl#custom) - Deep links -**AI Agent**: Before changing any code, check if the project contains any of the unsupported features. If it does, do not proceed. Inform the user of the unsupported feature and ask for further instructions. +**AI Agent**: Before changing any code, check if the project contains any of the +unsupported features. If it does, do not proceed. Inform the user of the +unsupported feature and ask for further instructions. ## Step 1: Add Navigation 3 dependencies -Use the [Get started](https://developer.android.com/guide/navigation/navigation-3/get-started) page to add the Navigation 3 dependencies to your project. The core dependencies are provided for you to copy. +Use the [Get started](https://developer.android.com/guide/navigation/navigation-3/get-started) page to add the Navigation 3 dependencies to your +project. The core dependencies are provided for you to copy. **lib.versions.toml** [versions] - nav3Core = "1.0.0" + nav3Core = "1.1.6" # If your screens depend on ViewModels, add the Nav3 Lifecycle ViewModel add-on library - lifecycleViewmodelNav3 = "2.10.0-rc01" + lifecycleViewmodelNav3 = "2.11.0" [libraries] # Core Navigation 3 libraries @@ -106,19 +120,32 @@ Use the [Get started](https://developer.android.com/guide/navigation/navigation- implementation(libs.androidx.lifecycle.viewmodel.navigation3) } -Also update the project's `minSdk` to 23 and the `compileSdk` to 36. You usually find these in `app/build.gradle.kts` or `lib.versions.toml`. +Also update the project's `minSdk` to 23 and the `compileSdk` to 36. You usually +find these in `app/build.gradle.kts` or `lib.versions.toml`. ## Step 2: Update navigation routes to implement the `NavKey` interface -Update every navigation [route](https://developer.android.com/guide/navigation#types) so that it implements the `NavKey` interface. This lets you use `rememberNavBackStack` to assist with [saving your navigation state](https://developer.android.com/guide/navigation/navigation-3/save-state). +Update every navigation [route](https://developer.android.com/guide/navigation#types) so that it implements the `NavKey` +interface. This lets you use `rememberNavBackStack` to assist with [saving your +navigation state](https://developer.android.com/guide/navigation/navigation-3/save-state). Before: - @Serializable data object RouteA + +```kotlin +@Serializable data object RouteA +``` + +
After: - @Serializable data object RouteA : NavKey + +```kotlin +@Serializable data object RouteA : NavKey +``` + +
> [!NOTE] > **Note:** The `@Serializable` annotation is provided by the KotlinX Serialization plugin. You can add this by following [these project setup steps](https://developer.android.com/guide/navigation/navigation-3/get-started#project-setup). @@ -127,139 +154,156 @@ After: ### Step 3.1: Create a navigation state holder -Copy the following code into a file named `NavigationState.kt`. Add your package name to match your project structure. +Copy the following code into a file named `NavigationState.kt`. Add your package +name to match your project structure. - // package com.example.project - import androidx.compose.runtime.Composable - import androidx.compose.runtime.MutableState - import androidx.compose.runtime.getValue - import androidx.compose.runtime.mutableStateOf - import androidx.compose.runtime.remember - import androidx.compose.runtime.saveable.rememberSerializable - import androidx.compose.runtime.setValue - import androidx.compose.runtime.snapshots.SnapshotStateList - import androidx.compose.runtime.toMutableStateList - import androidx.navigation3.runtime.NavBackStack - import androidx.navigation3.runtime.NavEntry - import androidx.navigation3.runtime.NavKey - import androidx.navigation3.runtime.rememberDecoratedNavEntries - import androidx.navigation3.runtime.rememberNavBackStack - import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator - import androidx.navigation3.runtime.serialization.NavKeySerializer - import androidx.savedstate.compose.serialization.serializers.MutableStateSerializer +```kotlin +// package com.example.project - /** - * Create a navigation state that persists config changes and process death. - */ - @Composable - fun rememberNavigationState( - startRoute: NavKey, - topLevelRoutes: Set - ): NavigationState { +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSerializable +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshots.SnapshotStateList +import androidx.compose.runtime.toMutableStateList +import androidx.navigation3.runtime.NavBackStack +import androidx.navigation3.runtime.NavEntry +import androidx.navigation3.runtime.NavKey +import androidx.navigation3.runtime.rememberDecoratedNavEntries +import androidx.navigation3.runtime.rememberNavBackStack +import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator +import androidx.navigation3.runtime.serialization.NavKeySerializer +import androidx.savedstate.compose.serialization.serializers.MutableStateSerializer - val topLevelRoute = rememberSerializable( - startRoute, topLevelRoutes, - serializer = MutableStateSerializer(NavKeySerializer()) - ) { - mutableStateOf(startRoute) - } +/** + * Create a navigation state that persists config changes and process death. + */ +@Composable +fun rememberNavigationState( + startRoute: NavKey, + topLevelRoutes: Set +): NavigationState { - val backStacks = topLevelRoutes.associateWith { key -> rememberNavBackStack(key) } - - return remember(startRoute, topLevelRoutes) { - NavigationState( - startRoute = startRoute, - topLevelRoute = topLevelRoute, - backStacks = backStacks - ) - } - } - - /** - * State holder for navigation state. - * - * @param startRoute - the start route. The user will exit the app through this route. - * @param topLevelRoute - the current top level route - * @param backStacks - the back stacks for each top level route - */ - class NavigationState( - val startRoute: NavKey, - topLevelRoute: MutableState, - val backStacks: Map> + val topLevelRoute = rememberSerializable( + startRoute, topLevelRoutes, + serializer = MutableStateSerializer(NavKeySerializer()) ) { - var topLevelRoute: NavKey by topLevelRoute - val stacksInUse: List - get() = if (topLevelRoute == startRoute) { - listOf(startRoute) - } else { - listOf(startRoute, topLevelRoute) - } + mutableStateOf(startRoute) } - /** - * Convert NavigationState into NavEntries. - */ - @Composable - fun NavigationState.toEntries( - entryProvider: (NavKey) -> NavEntry - ): SnapshotStateList> { + val backStacks = topLevelRoutes.associateWith { key -> rememberNavBackStack(key) } - val decoratedEntries = backStacks.mapValues { (_, stack) -> - val decorators = listOf( - rememberSaveableStateHolderNavEntryDecorator(), - ) - rememberDecoratedNavEntries( - backStack = stack, - entryDecorators = decorators, - entryProvider = entryProvider - ) + return remember(startRoute, topLevelRoutes) { + NavigationState( + startRoute = startRoute, + topLevelRoute = topLevelRoute, + backStacks = backStacks + ) + } +} + +/** + * State holder for navigation state. + * + * @param startRoute - the start route. The user will exit the app through this route. + * @param topLevelRoute - the current top level route + * @param backStacks - the back stacks for each top level route + */ +class NavigationState( + val startRoute: NavKey, + topLevelRoute: MutableState, + val backStacks: Map> +) { + var topLevelRoute: NavKey by topLevelRoute + val stacksInUse: List + get() = if (topLevelRoute == startRoute) { + listOf(startRoute) + } else { + listOf(startRoute, topLevelRoute) } +} - return stacksInUse - .flatMap { decoratedEntries[it] ?: emptyList() } - .toMutableStateList() +/** + * Convert NavigationState into NavEntries. + */ +@Composable +fun NavigationState.toEntries( + entryProvider: (NavKey) -> NavEntry +): SnapshotStateList> { + + val decoratedEntries = backStacks.mapValues { (_, stack) -> + val decorators = listOf( + rememberSaveableStateHolderNavEntryDecorator(), + ) + rememberDecoratedNavEntries( + backStack = stack, + entryDecorators = decorators, + entryProvider = entryProvider + ) } -**AI Agent** : `rememberSerializable` is correct. Do not change it to `rememberSaveable`. + return stacksInUse + .flatMap { decoratedEntries[it] ?: emptyList() } + .toMutableStateList() +} +``` -This file contains a state holder class named `NavigationState` and associated helper functions. It holds a set of top-level routes, each with its own back stack. Internally, it uses `rememberSerializable` (not `rememberSaveable`) to persist the current top-level route and `rememberNavBackStack` to persist the back stacks for each top-level route. +
+ +**AI Agent** : `rememberSerializable` is correct. Do not change it to +`rememberSaveable`. + +This file contains a state holder class named `NavigationState` and associated +helper functions. It holds a set of top-level routes, each with its own back +stack. Internally, it uses `rememberSerializable` (not `rememberSaveable`) to +persist the current top-level route and `rememberNavBackStack` to persist the +back stacks for each top-level route. ### Step 3.2: Create an object that modifies navigation state in response to events -Copy the following code into a file named `Navigator.kt`. Add your package name to match your project structure. +Copy the following code into a file named `Navigator.kt`. Add your package name +to match your project structure. - // package com.example.project - import androidx.navigation3.runtime.NavKey +```kotlin +// package com.example.project - /** - * Handles navigation events (forward and back) by updating the navigation state. - */ - class Navigator(val state: NavigationState){ - fun navigate(route: NavKey){ - if (route in state.backStacks.keys){ - // This is a top level route, just switch to it. - state.topLevelRoute = route - } else { - state.backStacks[state.topLevelRoute]?.add(route) - } - } +import androidx.navigation3.runtime.NavKey - fun goBack(){ - val currentStack = state.backStacks[state.topLevelRoute] ?: - error("Stack for ${state.topLevelRoute} not found") - val currentRoute = currentStack.last() - - // If we're at the base of the current route, go back to the start route stack. - if (currentRoute == state.topLevelRoute){ - state.topLevelRoute = state.startRoute - } else { - currentStack.removeLastOrNull() - } +/** + * Handles navigation events (forward and back) by updating the navigation state. + */ +class Navigator(val state: NavigationState) { + fun navigate(route: NavKey) { + if (route in state.backStacks.keys) { + // This is a top level route, just switch to it. + state.topLevelRoute = route + } else { + state.backStacks[state.topLevelRoute]?.add(route) } } + fun goBack() { + val currentStack = state.backStacks[state.topLevelRoute] + ?: error("Stack for ${state.topLevelRoute} not found") + val currentRoute = currentStack.last() + + // If we're at the base of the current route, go back to the start route stack. + if (currentRoute == state.topLevelRoute) { + state.topLevelRoute = state.startRoute + } else { + currentStack.removeLastOrNull() + } + } +} +``` + +
+ The `Navigator` class provides two navigation event methods: - `navigate` to a specific route. @@ -275,14 +319,22 @@ Both methods modify the `NavigationState`. ### Step 3.3: Create the `NavigationState` and `Navigator` -Create instances of `NavigationState` and `Navigator` with the same scope as your `NavController`. +Create instances of `NavigationState` and `Navigator` with the same scope as +your `NavController`. - val navigationState = rememberNavigationState( - startRoute = , - topLevelRoutes = - ) - val navigator = remember { Navigator(navigationState) } +```kotlin +val navigationState = rememberNavigationState( + // ... + startRoute = , + topLevelRoutes = + // ... +) + +val navigator = remember { Navigator(navigationState) } +``` + +
## Step 4: Replace `NavController` @@ -301,51 +353,87 @@ Replace `NavController` fields with `NavigationState` fields. | `currentBackStackEntry` `currentBackStackEntryAsState()` `currentBackStackEntryFlow` `currentDestination` | `backStacks[topLevelRoute].last()` | | Get the top level route: Traverse up the hierarchy from the current back stack entry to find it. | `topLevelRoute` | -Use `NavigationState.topLevelRoute` to determine the item that is currently selected in a navigation bar. +Use `NavigationState.topLevelRoute` to determine the item that is currently +selected in a navigation bar. Before: - val isSelected = navController.currentBackStackEntryAsState().value?.destination.isRouteInHierarchy(key::class) - fun NavDestination?.isRouteInHierarchy(route: KClass<*>) = - this?.hierarchy?.any { - it.hasRoute(route) - } ?: false +```kotlin +// ... +val isSelected = navController.currentBackStackEntryAsState().value?.destination.isRouteInHierarchy(key::class) +// ... + +fun NavDestination?.isRouteInHierarchy(route: KClass<*>) = + this?.hierarchy?.any { + it.hasRoute(route) + } ?: false +``` + +
After: - val isSelected = key == navigationState.topLevelRoute -Verify that you have removed all references to `NavController`, including any imports. +```kotlin +val isSelected = key == navigationState.topLevelRoute +``` + +
+ +Verify that you have removed all references to `NavController`, including +any imports. ### Step 4.1 Migrate lifecycle-aware logic -In Navigation 2, `NavBackStackEntry` implements `LifecycleOwner`, letting you listen to lifecycle events or collect flows in a lifecycle-aware manner using `navController.currentBackStackEntry`. +In Navigation 2, `NavBackStackEntry` implements `LifecycleOwner`, letting you +listen to lifecycle events or collect flows in a lifecycle-aware manner using +`navController.currentBackStackEntry`. -In Navigation 3, `NavDisplay` provides an entry-scoped `LifecycleOwner` through `LocalLifecycleOwner.current` to each destination's composable content. See [Destination lifecycle](https://developer.android.com/guide/navigation/navigation-3/basics#destination-lifecycle) for more information. +In Navigation 3, `NavDisplay` provides an entry-scoped `LifecycleOwner` +through `LocalLifecycleOwner.current` to each destination's composable +content. See [Destination lifecycle](https://developer.android.com/guide/navigation/navigation-3/basics#destination-lifecycle) for more information. -You should perform lifecycle-aware operations directly inside your destination's composable content by referencing `LocalLifecycleOwner.current`. +You should perform lifecycle-aware operations directly inside your destination's +composable content by referencing `LocalLifecycleOwner.current`. -For example, if you collect a flow in a lifecycle-aware manner using the back stack entry: +For example, if you collect a flow in a lifecycle-aware manner using the back +stack entry: Before: - // In your destination screen or host - val lifecycleOwner = navController.currentBackStackEntry - val state by flow.collectAsStateWithLifecycle(lifecycleOwner = lifecycleOwner) + +```kotlin +// In your destination screen or host +val lifecycleOwner = navController.currentBackStackEntry!! +val state by flow.collectAsStateWithLifecycle(lifecycleOwner = lifecycleOwner) +``` + +
After: - // Inside the destination composable - val state by flow.collectAsStateWithLifecycle() + +```kotlin +// Inside the destination composable +val state by flow.collectAsStateWithLifecycle() +``` + +
## Step 5: Move your destinations from `NavHost`'s `NavGraph` into an `entryProvider` -In Navigation 2, you [define your destinations](https://developer.android.com/guide/navigation/design#compose) using the [NavGraphBuilder DSL](https://developer.android.com/guide/navigation/design/kotlin-dsl#navgraphbuilder), usually inside `NavHost`'s trailing lambda. It is common to use extension functions here as described in [Encapsulate your navigation code](https://developer.android.com/guide/navigation/design/encapsulate). +In Navigation 2, you [define your destinations](https://developer.android.com/guide/navigation/design#compose) +using the [NavGraphBuilder DSL](https://developer.android.com/guide/navigation/design/kotlin-dsl#navgraphbuilder), +usually inside `NavHost`'s trailing lambda. It is common to use extension +functions here as described in [Encapsulate your navigation code](https://developer.android.com/guide/navigation/design/encapsulate). -In Navigation 3, you define your destinations using an `entryProvider`. This `entryProvider` resolves a route to a [`NavEntry`](https://developer.android.com/guide/navigation/navigation-3/basics#resolve-keys). Importantly, the `entryProvider` does not define parent-child relationships between entries. +In Navigation 3, you define your destinations using an `entryProvider`. This +`entryProvider` resolves a route to a [`NavEntry`](https://developer.android.com/guide/navigation/navigation-3/basics#resolve-keys). Importantly, the +`entryProvider` does not define parent-child relationships between entries. -In this migration guide, parent-child relationships are modelled as follows: +In this migration guide, parent-child relationships are modelled +as follows: - `NavigationState` has a set of top-level routes (the parent routes) and a stack for each one. It keeps track of the current top-level route and its associated stack. - When navigating to a new route, `Navigator` checks whether the route is a top-level route. If it is, the current top-level route and stack are updated. If it's not, it's a child route and is added to the current stack. @@ -355,83 +443,114 @@ In this migration guide, parent-child relationships are modelled as follows: ### Step 5.1: Create an `entryProvider` -Create an `entryProvider` [using the DSL](https://developer.android.com/guide/navigation/navigation-3/basics#entry-provider-DSL) at the same scope as the `NavigationState`. +Create an `entryProvider` [using the DSL](https://developer.android.com/guide/navigation/navigation-3/basics#entry-provider-DSL) at the same scope as the +`NavigationState`. - val entryProvider = entryProvider { - } +```kotlin +val entryProvider = entryProvider { + +} +``` + +
### Step 5.2: Move destinations into the `entryProvider` -For each destination defined inside `NavHost`, do the following based on the destination type: +For each destination defined inside `NavHost`, do the following based on the +destination type: - `navigation`: Delete it along with the route. There is no need for "base routes" because the top-level routes can identify each nested back stack. - `composable`: Move it into `entryProvider` and rename it to `entry`, retaining the type parameter. For example, `composable` becomes `entry`. - `dialog`: Do the same as `composable`, but add metadata to the entry as follows: `entry(metadata = DialogSceneStrategy.dialog())`. - [`bottomSheet`](https://developer.android.com/reference/kotlin/androidx/compose/material/navigation/package-summary#(androidx.navigation.NavGraphBuilder).bottomSheet(kotlin.String,kotlin.collections.List,kotlin.collections.List,kotlin.Function2)): [Follow the bottom sheet recipe here](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/bottomsheet). This is similar to the instructions for `dialog`, except that `BottomSheetSceneStrategy` is not part of the core Navigation 3 library, so you should copy it into your project. -**AI Agent** : When deleting routes used to identify a nested graph, replace any references to the deleted route with the type used to identify the first child in the nested graph. For example if the original code is `navigation{ composable{ ... } }`, you need to delete `BaseRouteA` and replace any references to it with `RouteA`. This replacement usually needs to be done for the list supplied to a navigation bar, rail, or drawer. +**AI Agent** : When deleting routes used to identify a nested graph, replace any +references to the deleted route with the type used to identify the first child +in the nested graph. For example if the original code is +`navigation{ composable{ ... } }`, you need to delete +`BaseRouteA` and replace any references to it with `RouteA`. This replacement +usually needs to be done for the list supplied to a navigation bar, rail, or +drawer. -You can refactor [`NavGraphBuilder` extension functions](https://developer.android.com/guide/navigation/design/encapsulate) to `EntryProviderScope` extension functions, and then move them. +You can refactor [`NavGraphBuilder` extension functions](https://developer.android.com/guide/navigation/design/encapsulate) to +`EntryProviderScope` extension functions, and then move them. Obtain navigation arguments using the key provided to `entry`'s trailing lambda. For example: - import androidx.navigation.NavDestination - import androidx.navigation.NavDestination.Companion.hasRoute - import androidx.navigation.NavDestination.Companion.hierarchy - import androidx.navigation.NavGraphBuilder - import androidx.navigation.compose.NavHost - import androidx.navigation.compose.composable - import androidx.navigation.compose.currentBackStackEntryAsState - import androidx.navigation.compose.dialog - import androidx.navigation.compose.navigation - import androidx.navigation.compose.rememberNavController - import androidx.navigation.navOptions - import androidx.navigation.toRoute - @Serializable data object BaseRouteA - @Serializable data class RouteA(val id: String) - @Serializable data object BaseRouteB - @Serializable data object RouteB - @Serializable data object RouteD +```kotlin +// ... +import androidx.navigation.NavDestination +import androidx.navigation.NavDestination.Companion.hasRoute +import androidx.navigation.NavDestination.Companion.hierarchy +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.navigation.compose.dialog +import androidx.navigation.compose.navigation +import androidx.navigation.compose.rememberNavController +import androidx.navigation.navOptions +import androidx.navigation.toRoute +// ... +@Serializable data object BaseRouteA +@Serializable data class RouteA(val id: String) +@Serializable data object BaseRouteB +@Serializable data object RouteB +@Serializable data object RouteD + +@Composable +fun NavHostSnippet(navController: NavHostController) { NavHost(navController = navController, startDestination = BaseRouteA){ - composable{ + composable{ entry -> val id = entry.toRoute().id ScreenA(title = "Screen has ID: $id") } featureBSection() dialog{ ScreenD() } } +} - fun NavGraphBuilder.featureBSection() { - navigation(startDestination = RouteB) { - composable { ScreenB() } - } +fun NavGraphBuilder.featureBSection() { + navigation(startDestination = RouteB) { + composable { ScreenB() } } +} +``` + +
becomes: - import androidx.navigation3.runtime.EntryProviderScope - import androidx.navigation3.runtime.NavKey - import androidx.navigation3.runtime.entryProvider - import androidx.navigation3.scene.DialogSceneStrategy - @Serializable data class RouteA(val id: String) : NavKey - @Serializable data object RouteB : NavKey - @Serializable data object RouteD : NavKey +```kotlin +// ... +import androidx.navigation3.runtime.EntryProviderScope +import androidx.navigation3.runtime.NavKey +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.scene.DialogSceneStrategy +// ... - val entryProvider = entryProvider { - entry{ key -> ScreenA(title = "Screen has ID: ${key.id}") } - featureBSection() - entry(metadata = DialogSceneStrategy.dialog()){ ScreenD() } - } +@Serializable data class RouteA(val id: String) : NavKey +@Serializable data object RouteB : NavKey +@Serializable data object RouteD : NavKey - fun EntryProviderScope.featureBSection() { - entry { ScreenB() } - } +val entryProvider = entryProvider { + entry{ key -> ScreenA(title = "Screen has ID: ${key.id}") } + featureBSection() + entry(metadata = DialogSceneStrategy.dialog()){ ScreenD() } +} + +fun EntryProviderScope.featureBSection() { + entry { ScreenB() } +} +``` + +
## Step 6: Replace `NavHost` with `NavDisplay` @@ -444,13 +563,16 @@ Replace `NavHost` with `NavDisplay`. For example: - import androidx.navigation3.ui.NavDisplay - NavDisplay( - entries = navigationState.toEntries(entryProvider), - onBack = { navigator.goBack() }, - sceneStrategies = remember { listOf(DialogSceneStrategy()) } - ) +```kotlin +NavDisplay( + entries = navigationState.toEntries(entryProvider), + onBack = { navigator.goBack() }, + sceneStrategies = remember { listOf(DialogSceneStrategy()) } +) +``` + +
## Step 7: Remove Navigation 2 dependencies @@ -458,4 +580,6 @@ Remove all Navigation 2 imports and library dependencies. ## Summary -Congratulations! Your project is now migrated to Navigation 3. If you or your AI agent has run into any problems using this guide, [file a bug here](https://issuetracker.google.com/issues/new?component=1750212&template=2102223&title=%5BMigration%5D). \ No newline at end of file +Congratulations! Your project is now migrated to Navigation 3. If you or your AI +agent has run into any problems using this guide, [file a bug +here](https://issuetracker.google.com/issues/new?component=1750212&template=2102223&title=%5BMigration%5D). \ No newline at end of file diff --git a/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/animations.md b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/animations.md index 579a539..5e15bd4 100644 --- a/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/animations.md +++ b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/animations.md @@ -144,6 +144,4 @@ class AnimatedActivity : ComponentActivity() { } } } - - ``` \ No newline at end of file diff --git a/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/basic.md b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/basic.md index 4ab55aa..087d5cf 100644 --- a/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/basic.md +++ b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/basic.md @@ -86,6 +86,4 @@ class BasicActivity : ComponentActivity() { } } } - - ``` \ No newline at end of file diff --git a/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/basicdsl.md b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/basicdsl.md index 461afd7..2067c08 100644 --- a/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/basicdsl.md +++ b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/basicdsl.md @@ -82,6 +82,4 @@ class BasicDslActivity : ComponentActivity() { } } } - - ``` \ No newline at end of file diff --git a/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/basicsaveable.md b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/basicsaveable.md index 4575a71..fd2a72d 100644 --- a/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/basicsaveable.md +++ b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/basicsaveable.md @@ -87,6 +87,4 @@ class BasicSaveableActivity : ComponentActivity() { } } } - - ``` \ No newline at end of file diff --git a/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/bottomsheet.md b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/bottomsheet.md index 1ae419a..aa6b456 100644 --- a/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/bottomsheet.md +++ b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/bottomsheet.md @@ -102,8 +102,6 @@ class BottomSheetActivity : ComponentActivity() { } } } - - ``` ``` @@ -194,5 +192,4 @@ class BottomSheetSceneStrategy : SceneStrategy { } } - ``` \ No newline at end of file diff --git a/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/common-ui.md b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/common-ui.md index da4f696..d49b399 100644 --- a/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/common-ui.md +++ b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/common-ui.md @@ -197,6 +197,4 @@ class TopLevelBackStack(startKey: T) { } } - - ``` \ No newline at end of file diff --git a/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/conditional-transitions.md b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/conditional-transitions.md index 497f208..c83cdca 100644 --- a/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/conditional-transitions.md +++ b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/conditional-transitions.md @@ -179,6 +179,4 @@ private fun swipeRight(): ContentTransform = private fun swipeDown(): ContentTransform = slideInVertically(initialOffsetY = { -it }) togetherWith slideOutVertically(targetOffsetY = { it }) - - ``` \ No newline at end of file diff --git a/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/conditional.md b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/conditional.md index 0bd6bca..60848b3 100644 --- a/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/conditional.md +++ b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/conditional.md @@ -176,7 +176,6 @@ fun rememberNavBackStack(vararg elements: T): NavBackStack { NavBackStack(*elements) } } - ``` ``` @@ -228,5 +227,4 @@ class Navigator( fun goBack() = backStack.removeLastOrNull() } - ``` \ No newline at end of file diff --git a/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-custommatcher.md b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-custommatcher.md index cf2d0c1..fcfea49 100644 --- a/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-custommatcher.md +++ b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-custommatcher.md @@ -11,9 +11,11 @@ This recipe consists of two activities: ## Key Concepts -1. **Custom `RequestExtrasKey`** : `JsonDeepLinkMatcherKey` defines a custom extra key implementing `RequestExtrasKey` to type-safely store and read serialized JSON payloads in `DeepLinkRequest.extras`. +1. **Custom `RequestExtrasKey`** : + `JsonDeepLinkMatcherKey` defines a custom extra key implementing `RequestExtrasKey` to type-safely store and read serialized JSON payloads in `DeepLinkRequest.extras`. -2. **Custom `DeepLinkMatcher`** : `JsonDeepLinkMatcher` extends `DeepLinkMatcher>` and implements `matchRequest(request)` to extract `request.extras[JsonDeepLinkMatcherKey]` and decode it into a strongly typed `NavKey` using Kotlinx Serialization. +2. **Custom `DeepLinkMatcher`** : + `JsonDeepLinkMatcher` extends `DeepLinkMatcher>` and implements `matchRequest(request)` to extract `request.extras[JsonDeepLinkMatcherKey]` and decode it into a strongly typed `NavKey` using Kotlinx Serialization. [![](https://developer.android.com/static/images/picto-icons/code.svg) Explore View the full recipe on GitHub.](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/deeplink/usecases/matcher) @@ -102,8 +104,6 @@ internal class JsonDeepLinkMatcher(val serializer: KSerializer): D } } } - - ``` ``` @@ -173,6 +173,4 @@ private inline fun createJsonDeepLinkMatcher(): JsonDeepLin val serializer = serializer() return JsonDeepLinkMatcher(serializer) } - - ``` \ No newline at end of file diff --git a/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-staticuri.md b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-staticuri.md index 5b9aa71..0f35b99 100644 --- a/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-staticuri.md +++ b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-staticuri.md @@ -55,8 +55,6 @@ class StaticUriDeepLinkActivity : ComponentActivity() { } } } - - ``` ``` @@ -139,5 +137,4 @@ private val HOME_MATCHER = UriDeepLinkMatcher( uriPattern = DeepLinkUri(HOME_URI), serializer = serializer(), ) - ``` \ No newline at end of file diff --git a/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-syntheticbackstack.md b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-syntheticbackstack.md index 6eae0e3..5ccc370 100644 --- a/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-syntheticbackstack.md +++ b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-syntheticbackstack.md @@ -1,18 +1,25 @@ # Deep Link Synthetic BackStack Recipe -This recipe demonstrates how to apply the principles of navigation in the context of deep links by managing a synthetic backStack and Task stacks. +This recipe demonstrates how to apply the principles of navigation in the context of deep links by +managing a synthetic backStack and Task stacks. # Recipe Structure -This recipe simulates a real-world scenario where "App A" deep links into "App B". +This recipe simulates a real-world scenario where "App A" deep links +into "App B". -"App A" is simulated by the module [syntheticbackstack](https://developer.android.com/app/src/main/java/com/example/nav3recipes/deeplink/handlerequests/syntheticbackstack), which contains the `SyntheticBackStackDeepLinkActivity` that allows you to create a deeplink intent and trigger that in either the existing Task, or in a new Task. +"App A" is simulated by the module [syntheticbackstack](https://developer.android.com/app/src/main/java/com/example/nav3recipes/deeplink/handlerequests/syntheticbackstack), which +contains the `SyntheticBackStackDeepLinkActivity` that allows you to create a deeplink intent and +trigger that in either the existing Task, or in a new Task. -"App B" is simulated by the module [syntheticbackstackapp](https://developer.android.com/syntheticbackstackapp/src/main/java/com/example/nav3recipes/deeplink/syntheticbackstack), which contains the `SyntheticBackStackAppActivity` that you deeplink into. That module shows you how to build a synthetic backStack and how to manage the Task stack properly in order to support both Back and Up buttons. +"App B" is simulated by the module [syntheticbackstackapp](https://developer.android.com/syntheticbackstackapp/src/main/java/com/example/nav3recipes/deeplink/syntheticbackstack), which contains +the `SyntheticBackStackAppActivity` that you deeplink into. That module shows you how to build a synthetic backStack +and how to manage the Task stack properly in order to support both Back and Up buttons. # How to Use -Ensure both the main `app` and `syntheticbackstackapp` are installed on the emulator or connected device. Ensure that the installed `syntheticbackstackapp` supports the `"www.nav3deeplink.com"` link. +Ensure both the main `app` and `syntheticbackstackapp` are installed on the emulator or connected device. Ensure that the installed `syntheticbackstackapp` supports +the `"www.nav3deeplink.com"` link. On the recipe's landing page, choose the filters and click the button to deep link. It should bring you to the Activity of `syntheticbackstackapp`. @@ -40,11 +47,13 @@ To see behavior of `New Task`: # Core implementation -The core helper functions for navigateUp and building synthetic backStack can be found [here](https://developer.android.com/syntheticbackstackapp/src/main/java/com/example/nav3recipes/deeplink/syntheticbackstack/util) +The core helper functions for navigateUp and building synthetic backStack can be +found [here](https://developer.android.com/syntheticbackstackapp/src/main/java/com/example/nav3recipes/deeplink/syntheticbackstack/util) # Further Read -Check out the [deep link guide](https://developer.android.com/docs/deeplink-guide) for a comprehensive guide on Deep linking principles and how to apply them in Navigation 3. +Check out the [deep link guide](https://developer.android.com/docs/deeplink-guide) for a +comprehensive guide on Deep linking principles and how to apply them in Navigation 3. [![](https://developer.android.com/static/images/picto-icons/code.svg) Explore View the full recipe on GitHub.](https://github.com/android/nav3-recipes/tree/main/app/src/main/java/com/example/nav3recipes/deeplink/handlerequests/syntheticbackstack) ``` @@ -172,6 +181,4 @@ private val MENU_OPTIONS_LOCATION = mapOf( private val MENU_OPTIONS_TASK_STACK = mapOf( TAG_TASK_STACK to listOf(TAG_CURRENT_TASK, TAG_NEW_TASK), ) - - ``` \ No newline at end of file diff --git a/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-uriarguments.md b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-uriarguments.md index 5113ed5..2a670b8 100644 --- a/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-uriarguments.md +++ b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/deeplinks-uriarguments.md @@ -80,7 +80,6 @@ internal data class SearchKey( ): NavRecipeKey { override val name: String = STRING_LITERAL_SEARCH } - ``` ``` @@ -182,7 +181,6 @@ class MainActivity : ComponentActivity() { } } } - ``` ``` @@ -363,8 +361,6 @@ private val MENU_OPTIONS_SEARCH = mapOf( private val MENU_LABELS_SEARCH = listOf(SearchKey::ageMin.name, SearchKey::ageMax.name) - - ``` ``` @@ -391,6 +387,4 @@ internal val URL_SEARCH = "$PATH_BASE/$PATH_SEARCH" + "&${SearchKey::ageMax.name}={${SearchKey::ageMax.name}}" + "&${SearchKey::firstName.name}={${SearchKey::firstName.name}}" + "&${SearchKey::location.name}={${SearchKey::location.name}}" - - ``` \ No newline at end of file diff --git a/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/dialog.md b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/dialog.md index 543c21f..6ef8efe 100644 --- a/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/dialog.md +++ b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/dialog.md @@ -104,6 +104,4 @@ class DialogActivity : ComponentActivity() { } } } - - ``` \ No newline at end of file diff --git a/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/lifecycle-owner.md b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/lifecycle-owner.md index 90c4317..055ed6e 100644 --- a/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/lifecycle-owner.md +++ b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/lifecycle-owner.md @@ -182,6 +182,4 @@ private fun LogLifecycleEffect(screenName: String) { } // Note that ON_DESTROY event is not observable from composables. } - - ``` \ No newline at end of file diff --git a/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/material-listdetail.md b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/material-listdetail.md index 032a997..fbaae79 100644 --- a/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/material-listdetail.md +++ b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/material-listdetail.md @@ -138,6 +138,4 @@ class MaterialListDetailActivity : ComponentActivity() { } } } - - ``` \ No newline at end of file diff --git a/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/material-supportingpane.md b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/material-supportingpane.md index d20f52e..58a6a7c 100644 --- a/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/material-supportingpane.md +++ b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/material-supportingpane.md @@ -142,6 +142,4 @@ class MaterialSupportingPaneActivity : ComponentActivity() { } } } - - ``` \ No newline at end of file diff --git a/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/modular-hilt.md b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/modular-hilt.md index e4550b8..fcc94a2 100644 --- a/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/modular-hilt.md +++ b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/modular-hilt.md @@ -75,8 +75,6 @@ private fun ProfileScreen() { ) } } - - ``` ``` @@ -120,8 +118,6 @@ class HiltModularActivity : ComponentActivity() { } } } - - ``` ``` @@ -240,8 +236,6 @@ private fun ConversationDetailScreen( } } } - - ``` ``` @@ -267,7 +261,6 @@ class Navigator(startDestination: Any) { backStack.removeLastOrNull() } } - ``` ``` @@ -287,6 +280,4 @@ object AppModule { @ActivityRetainedScoped fun provideNavigator() : Navigator = Navigator(startDestination = ConversationList) } - - ``` \ No newline at end of file diff --git a/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/modular-koin.md b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/modular-koin.md index 6e59bdc..d1e7ad6 100644 --- a/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/modular-koin.md +++ b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/modular-koin.md @@ -66,8 +66,6 @@ private fun ProfileScreen() { ) } } - - ``` ``` @@ -183,8 +181,6 @@ private fun ConversationDetailScreen( } } } - - ``` ``` @@ -204,7 +200,6 @@ class Navigator(startDestination: Any) { backStack.removeLastOrNull() } } - ``` ``` @@ -222,7 +217,6 @@ val appModule = module { } } } - ``` ``` @@ -290,6 +284,4 @@ class KoinModularActivity : ComponentActivity(), AndroidScopeComponent, KoinComp } } - - ``` \ No newline at end of file diff --git a/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/multiple-backstacks.md b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/multiple-backstacks.md index 8831896..aca3404 100644 --- a/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/multiple-backstacks.md +++ b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/multiple-backstacks.md @@ -78,8 +78,6 @@ class Navigator(val state: NavigationState){ } } } - - ``` ``` @@ -216,8 +214,6 @@ class NavigationState( listOf(startRoute, topLevelRoute) } } - - ``` ``` @@ -346,8 +342,6 @@ class MultipleStacksActivity : ComponentActivity() { } } } - - ``` ``` @@ -471,6 +465,4 @@ fun EntryProviderScope.featureCSection( } } } - - ``` \ No newline at end of file diff --git a/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/passingarguments.md b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/passingarguments.md index b00cd74..2640a22 100644 --- a/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/passingarguments.md +++ b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/passingarguments.md @@ -133,8 +133,6 @@ class RouteBViewModel @AssistedInject constructor( fun create(navKey: RouteB): RouteBViewModel } } - - ``` # Passing Arguments to ViewModels (Basic) @@ -259,8 +257,6 @@ class RouteBViewModel( } } } - - ``` # Passing Arguments to ViewModels (Koin) @@ -372,5 +368,4 @@ fun ScreenB(viewModel: RouteBViewModel) { } class RouteBViewModel(val navKey: RouteB) : ViewModel() - ``` \ No newline at end of file diff --git a/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/results-event.md b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/results-event.md index a8c510e..ea7d27d 100644 --- a/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/results-event.md +++ b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/results-event.md @@ -41,7 +41,6 @@ import androidx.lifecycle.ViewModel class HomeViewModel : ViewModel() { var person by mutableStateOf(null) } - ``` ``` @@ -71,7 +70,6 @@ data object Home : NavKey @Serializable class PersonDetailsForm : NavKey - ``` ``` @@ -97,7 +95,6 @@ import kotlinx.serialization.Serializable @Serializable data class Person(val name: String, val favoriteColor: String) - ``` ``` @@ -183,7 +180,6 @@ fun PersonDetailsScreen( } } } - ``` ``` @@ -274,6 +270,4 @@ class ResultEventActivity : ComponentActivity() { } } } - - ``` \ No newline at end of file diff --git a/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/results-state.md b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/results-state.md index 4380a9d..a73e148 100644 --- a/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/results-state.md +++ b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/results-state.md @@ -41,7 +41,6 @@ import androidx.lifecycle.ViewModel class HomeViewModel : ViewModel() { var person by mutableStateOf(null) } - ``` ``` @@ -71,7 +70,6 @@ data object Home : NavKey @Serializable class PersonDetailsForm : NavKey - ``` ``` @@ -97,7 +95,6 @@ import kotlinx.serialization.Serializable @Serializable data class Person(val name: String, val favoriteColor: String) - ``` ``` @@ -183,7 +180,6 @@ fun PersonDetailsScreen( } } } - ``` ``` @@ -267,6 +263,4 @@ class ResultStateActivity : ComponentActivity() { } } } - - ``` \ No newline at end of file diff --git a/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/scenes-listdetail.md b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/scenes-listdetail.md index 4a2947b..32063b3 100644 --- a/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/scenes-listdetail.md +++ b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/scenes-listdetail.md @@ -168,8 +168,6 @@ class ListDetailSceneStrategy(val windowSizeClass: WindowSizeClass) : S ) } } - - ``` ``` @@ -289,7 +287,6 @@ private fun NavBackStack.addDetail(detailRoute: ConversationDetail) { removeIf { it is ConversationDetail } add(detailRoute) } - ``` ``` @@ -435,5 +432,4 @@ fun ProfileScreen() { ) } } - ``` \ No newline at end of file diff --git a/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/scenes-twopane.md b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/scenes-twopane.md index 513494a..fed0bf9 100644 --- a/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/scenes-twopane.md +++ b/navigation/navigation-3/references/android/guide/navigation/navigation-3/recipes/scenes-twopane.md @@ -123,8 +123,6 @@ class TwoPaneSceneStrategy(val windowSizeClass: WindowSizeClass) : Scen } - - ``` ``` @@ -243,6 +241,4 @@ private fun NavBackStack.addProductRoute(productId: Int) { add(productRoute) } } - - ``` \ No newline at end of file diff --git a/navigation/navigation-3/references/android/guide/navigation/type-safe-destinations.md b/navigation/navigation-3/references/android/guide/navigation/type-safe-destinations.md index 8959d00..c570000 100644 --- a/navigation/navigation-3/references/android/guide/navigation/type-safe-destinations.md +++ b/navigation/navigation-3/references/android/guide/navigation/type-safe-destinations.md @@ -1,8 +1,11 @@ -This guide outlines the process of replacing string-based routes with serializable Kotlin types to achieve compile-time safety and eliminate runtime crashes caused by typos or incorrect argument types. +This guide outlines the process of replacing string-based routes with +serializable Kotlin types to achieve compile-time safety and eliminate runtime +crashes caused by typos or incorrect argument types. ## Prerequisites -Before starting the migration, verify that your project meets the following requirements: +Before starting the migration, verify that your project meets the following +requirements: 1. **Navigation version**: Update to Jetpack Navigation 2.8.0 or higher 2. **Kotlin serialization plugin**: @@ -40,7 +43,8 @@ Replace your constant route strings with `@Serializable` objects and classes. ## Step 2: Update the NavHost Configuration -Update your `NavHost` to use the new generic types in the `composable` and `dialog` function. +Update your `NavHost` to use the new generic types in the `composable` and +`dialog` function. **Before:** @@ -79,7 +83,8 @@ Replace string-interpolated navigation calls with class instances. ## Step 4: Accessing Arguments in ViewModels -If you use a `ViewModel`, you can now extract the route object directly from the `SavedStateHandle`. +If you use a `ViewModel`, you can now extract the route object directly from the +`SavedStateHandle`. **Implementation:** @@ -93,9 +98,10 @@ If you use a `ViewModel`, you can now extract the route object directly from the ## Step 5: (Advanced) Handling Custom Types -If you need to pass complex data classes (not just primitives), you must define a custom `NavType`. +If you need to pass complex data classes (not just primitives), you must define +a custom `NavType`. -1. **Create the Custom Type** : \`\`\`kotlin val SearchFilterType = object : NavType (isNullableAllowed = false) { override fun get(bundle: Bundle, key: String): SearchFilter? = Json.decodeFromString(bundle.getString(key) ?: return null) +1. **Create the Custom Type** : \`\`\`kotlin val SearchFilterType = object : NavType(isNullableAllowed = false) { override fun get(bundle: Bundle, key: String): SearchFilter? = Json.decodeFromString(bundle.getString(key) ?: return null) override fun parseValue(value: String): SearchFilter = Json.decodeFromString(Uri.decode(value)) @@ -118,5 +124,6 @@ If you need to pass complex data classes (not just primitives), you must define - **Sealed Hierarchies**: For large apps, group your routes using a sealed interface or class to keep the navigation structure organized - **Object Instances** : For routes without parameters, always use `object` instead of `class` to avoid unnecessary allocations -- **Nullable Types** : The new API supports nullable types (for example, `data class Search(val query: String?)`) and provides default values automatically +- **Nullable Types** : The new API supports nullable types (for example, `data + class Search(val query: String?)`) and provides default values automatically - **Testing** : Use `navController.currentBackStackEntry?.hasRoute()` to check the current destination in a type-safe manner during UI tests \ No newline at end of file diff --git a/performance/r8-analyzer/SKILL.md b/performance/r8-analyzer/SKILL.md index f3ab821..e72b03a 100644 --- a/performance/r8-analyzer/SKILL.md +++ b/performance/r8-analyzer/SKILL.md @@ -7,7 +7,7 @@ description: Analyzes Android build files and R8 keep rules to identify redundan license: Complete terms in LICENSE.txt metadata: author: Google LLC - last-updated: '2026-08-06' + last-updated: '2026-08-15' keywords: - R8 - proguard @@ -25,7 +25,8 @@ metadata: ## Step 2. Analysis path selection -- Inspect `build.gradle`, `build.gradle.kts`, and `gradle.properties` and `libs.versions.toml` to get the AGP and R8 versions. +- Inspect `build.gradle`, `build.gradle.kts`, and `gradle.properties` and + `libs.versions.toml` to get the AGP and R8 versions. - **If AGP \>= 9.3.0** : Proceed to **Path A (Standalone Task)**. diff --git a/performance/r8-analyzer/references/CONFIGURATION-ANALYZER.md b/performance/r8-analyzer/references/CONFIGURATION-ANALYZER.md index 84cf3d1..4039dcf 100644 --- a/performance/r8-analyzer/references/CONFIGURATION-ANALYZER.md +++ b/performance/r8-analyzer/references/CONFIGURATION-ANALYZER.md @@ -6,13 +6,17 @@ On each step, keep the user informed of the progress by displaying the output. ### 2. Generate proto -The report and files must be generated at `{project_root}/tmp/r8analysis`. If the folder is not present, create it. For example: +The report and files must be generated at `{project_root}/tmp/r8analysis`. If +the folder is not present, create it. For example: mkdir -p "$PWD/tmp/r8analysis" ### 3. Remove existing files -To make sure that this invocation doesn't source data from previous runs, remove the intermediate files `keepruleradius.json` and `analysis_result.txt` and remove the proto files in the `{project_root}/tmp/r8analysis` folder. Example bash commands: +To make sure that this invocation doesn't source data from previous runs, remove +the intermediate files `keepruleradius.json` and `analysis_result.txt` and +remove the proto files in the `{project_root}/tmp/r8analysis` folder. Example +bash commands: # Remove the intermediate JSON and the directory containing protobuf files rm tmp/r8analysis/keepruleradius.json @@ -25,14 +29,19 @@ To make sure that this invocation doesn't source data from previous runs, remove ### 4. Generate the Configuration Analyzer report -Run the R8 enabled build with the system property "-Dcom.android.tools.r8.dumpkeepradiustodirectory=$PWD/tmp/r8analysis" to generate Configuration Analyzer report +Run the R8 enabled build with the system property +"-Dcom.android.tools.r8.dumpkeepradiustodirectory=$PWD/tmp/r8analysis" to +generate Configuration Analyzer report ./gradlew assembleRelease \ -Dcom.android.tools.r8.dumpkeepradiustodirectory=$PWD/tmp/r8analysis ### 5. Convert to JSON -To convert the generated protobuf files in `{project_root}/tmp/r8analysis` into json, run the following script. The json must be generated in `{project_root}/tmp/r8analysis`. Ensure `keep_radius_pb2.py` (from Step 10) is in the same directory. +To convert the generated protobuf files in `{project_root}/tmp/r8analysis` into +json, run the following script. The json must be generated in +`{project_root}/tmp/r8analysis`. Ensure `keep_radius_pb2.py` (from Step 10) is +in the same directory. import sys import os @@ -84,7 +93,8 @@ To convert the generated protobuf files in `{project_root}/tmp/r8analysis` into ### 6. Analyze -Run the following analysis script on the generated JSON to get the impact of the keep rules and sort it. +Run the following analysis script on the generated JSON to get the impact of the +keep rules and sort it. import json, sys @@ -140,7 +150,8 @@ Outputs `analysis_result.txt` containing scores and rule impacts. ### 7. Report impactful rules -Identify the keep rules with the highest impact and the subsumed rules using the following script. +Identify the keep rules with the highest impact and the subsumed rules using the +following script. import json, sys @@ -187,22 +198,27 @@ Identify the keep rules with the highest impact and the subsumed rules using the if __name__ == "__main__": report("tmp/r8analysis/keepruleradius.json") -Add this data to the `analysis_result.txt` with the top impactful rules and subsumed rules. +Add this data to the `analysis_result.txt` with the top impactful rules and +subsumed rules. ### 8. Compare with previous report -If `{project_root}/tmp/r8analysis/history.txt` exists, use the following script to compare the previous run. Use this to compare with the current values +If `{project_root}/tmp/r8analysis/history.txt` exists, use the following script +to compare the previous run. Use this to compare with the current values ### 9. Remove generated files -After the final report and analysis results are generated, remove the intermediate files `keepruleradius.json` and `analysis_result.txt` and remove the proto files in "{project_root}/tmp/r8analysis" folder +After the final report and analysis results are generated, remove the +intermediate files `keepruleradius.json` and `analysis_result.txt` and remove +the proto files in "{project_root}/tmp/r8analysis" folder rm tmp/r8analysis/keepruleradius.json rm tmp/r8analysis/*.pb ### 10. Protobuf Python bindings -The following script `keep_radius_pb2.py` is required by the conversion script in Step 5. +The following script `keep_radius_pb2.py` is required by the conversion script +in Step 5. from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool diff --git a/performance/r8-analyzer/references/CONFIGURATION.md b/performance/r8-analyzer/references/CONFIGURATION.md index 81263a9..ae04947 100644 --- a/performance/r8-analyzer/references/CONFIGURATION.md +++ b/performance/r8-analyzer/references/CONFIGURATION.md @@ -1,8 +1,12 @@ -To achieve maximum utilization of R8, the codebase must be configured correctly depending on the build script language (Kotlin DSL versus Groovy DSL). +To achieve maximum utilization of R8, the codebase must be configured correctly +depending on the build script language (Kotlin DSL versus Groovy DSL). ## 1. App Modules (`com.android.application`) -The app's `build.gradle` or `build.gradle.kts` file must enable minification and resource shrinking within the `release` build type or the apps custom build type for release and performance testing. It MUST use the optimized default file (`proguard-android-optimize.txt`). +The app's `build.gradle` or `build.gradle.kts` file must enable minification +and resource shrinking within the `release` build type or the apps custom build +type for release and performance testing. It MUST use the optimized default file +(`proguard-android-optimize.txt`). **Kotlin DSL (`build.gradle.kts`):** @@ -34,6 +38,7 @@ The app's `build.gradle` or `build.gradle.kts` file must enable minification and - **AGP 8.0+** : Enabled by default. Ensure `android.enableR8.fullMode=false` is **NOT** present. - **Pre-AGP 8.0** : Explicitly enable with `android.enableR8.fullMode=true`. -**Optimized Resource Shrinking:** If the AGP version of the project is less than 9.0 and more than 8.6, explicitly enable the new resource shrinker: +**Optimized Resource Shrinking:** If the AGP version of the project is less than +9.0 and more than 8.6, explicitly enable the new resource shrinker: android.r8.optimizedResourceShrinking=true \ No newline at end of file diff --git a/performance/r8-analyzer/references/KEEP-RULES-IMPACT-HIERARCHY.md b/performance/r8-analyzer/references/KEEP-RULES-IMPACT-HIERARCHY.md index c01a9be..2e07317 100644 --- a/performance/r8-analyzer/references/KEEP-RULES-IMPACT-HIERARCHY.md +++ b/performance/r8-analyzer/references/KEEP-RULES-IMPACT-HIERARCHY.md @@ -1,38 +1,53 @@ -Keep rules prevent optimization of R8, these rules are listed in the order of the scope of what it retains in the codebase. +Keep rules prevent optimization of R8, these rules are listed in the order of +the scope of what it retains in the codebase. ## 1. Package-Wide Wildcards -The following types of keep rules prevents all the optimization of R8 in a package, these must be avoided at any costs and must be refined to target a specific class or classes. +The following types of keep rules prevents all the optimization of R8 in a +package, these must be avoided at any costs and must be refined to target a +specific class or classes. -keep class com.example.package.** { *; } - Prevents optimization of all the classess including members in the package and subpackages -keep class com.example.package.* { *; } - Prevents optimization of all the classes including members in the package -keep class **.package.** { *; } - Prevents optimization of all the classess including members in all the package containing name - package. -Depending on the package level the number of classes gets affected changes, so if the package level is higher, more classes are affected. Suggest to refine the keep rule +Depending on the package level the number of classes gets affected changes, so +if the package level is higher, more classes are affected. Suggest to refine +the keep rule ## 2. Inversion operator -Avoid using the inversion operator ! in keep rules because it will unintentionally prevent optimization in every class in your application. So if you have any keep rule with !operator, make sure you remove that with a narrow and specific keep rule +Avoid using the inversion operator ! in keep rules because it will +unintentionally prevent optimization in every class in your application. So if +you have any keep rule with !operator, make sure you remove that with a narrow +and specific keep rule -keep class !com.example.MyClass{*;} -This keeps the entire app other than this class. Optimization are disabled for the entire class other than this class. +This keeps the entire app +other than this class. Optimization are disabled for the entire class other +than this class. ## 3. Keep Rules for both class and members -Keep rules with -keep option and wildcard(`*`) inside braces forces R8 to retain specific classes and their members exactly as defined. These type of keep rules prevent any optimization in the entire class and keeps the entire class +Keep rules with -keep option and wildcard(`*`) inside braces forces R8 to retain +specific classes and their members exactly as defined. These type of keep rules +prevent any optimization in the entire class and keeps the entire class -keep class com.example.MyClass { *; } ## 4. Keepclassmembers -Keep rules with -keepclassmembers and wildcard(`*`) inside braces option Forces R8 to retain the members that are defined. +Keep rules with -keepclassmembers and wildcard(`*`) inside braces option Forces +R8 to retain the members that are defined. -keepclassmembers class com.example.MyClass { *; } ## 5. Modifiers with Keep Specification --Keeps the class and **all** members, but uses modifiers to allow specific optimizations (like obfuscation). Retains significant code (members) but allows some flexibility. +-Keeps the class and **all** members, but uses modifiers to allow specific +optimizations (like obfuscation). Retains significant code (members) but allows +some flexibility. -keep,allowobfuscation class com.example.MyClass { *; } -keep,allowshrinking class com.example.MyClass { *; } @@ -45,13 +60,15 @@ Keeps the class and modifier but no optimizations are enabled ## 7. Class-Name Only Preservation -Keeps only the class name. R8 will remove all methods and fields if they are not used. +Keeps only the class name. R8 will remove all methods and fields if they are not +used. -keep class com.example.MyClass ## 8. Modifiers without Member Specification -Keeps the class entry point using modifiers, but implies no specific member retention logic in the rule itself +Keeps the class entry point using modifiers, but implies no specific member +retention logic in the rule itself -keep,allowobfuscation class com.example.MyClass -keep,allowshrinking class com.example.MyClass @@ -59,7 +76,8 @@ Keeps the class entry point using modifiers, but implies no specific member rete ## 9. Conditional Keep Rules -Only triggers if specific conditions are met (e.g., if class members exist). These are the most narrow and optimization-friendly rules. +Only triggers if specific conditions are met (e.g., if class members exist). +These are the most narrow and optimization-friendly rules. -keepclassmembers class com.example.MyClass { ; } -keepclasseswithmembers class * { native ; } \ No newline at end of file diff --git a/performance/r8-analyzer/references/REDUNDANT-RULES.md b/performance/r8-analyzer/references/REDUNDANT-RULES.md index 0bfe0e2..0021890 100644 --- a/performance/r8-analyzer/references/REDUNDANT-RULES.md +++ b/performance/r8-analyzer/references/REDUNDANT-RULES.md @@ -1,20 +1,36 @@ -This document outlines common "bad" or redundant keep rules for standard Android development and popular libraries. Modern toolchains and libraries include their own consumer keep rules embedded in their AAR/JAR files, making many manual configurations unnecessary or even harmful to code optimization. +This document outlines common "bad" or redundant keep rules for standard Android +development and popular libraries. Modern toolchains and libraries include their +own consumer keep rules embedded in their AAR/JAR files, making many manual +configurations unnecessary or even harmful to code optimization. *** ** * ** *** ## Case: Global Keep Rules -**Common Mistakes:** `proguard -dontshrink -dontobfuscate -dontoptimize` +**Common Mistakes:** +`proguard +-dontshrink +-dontobfuscate +-dontoptimize` -**The Fix:** These keep rules completely disable the core optimizations of R8 for the entire codebase. They must be removed from the codebase. +**The Fix:** These keep rules completely disable the core optimizations of R8 +for the entire codebase. They must be removed from the codebase. *** ** * ** *** ## Case: Android Components -Keep rules required for Android components like Activity, Fragment, ViewModel, Views, Services or Broadcast receivers are redundant. AAPT2 and R8 contain the logic to automatically keep components declared in the `AndroidManifest.xml` or referenced in XML layout files. +Keep rules required for Android components like Activity, Fragment, ViewModel, +Views, Services or Broadcast receivers are redundant. AAPT2 and R8 contain the +logic to automatically keep components declared in the `AndroidManifest.xml` or +referenced in XML layout files. -**Common Mistakes:** `proguard -keep public class * extends android.app.Activity -keep public class * extends android.app.Service -keep public class * extends android.view.View -keepclassmembers class * extends android.app.Fragment { public void *(android.view.View); }` +**Common Mistakes:** +`proguard +-keep public class * extends android.app.Activity +-keep public class * extends android.app.Service +-keep public class * extends android.view.View +-keepclassmembers class * extends android.app.Fragment { public void *(android.view.View); }` **The Fix:** Delete these manual rules. AAPT2 handles this automatically. @@ -22,11 +38,18 @@ Keep rules required for Android components like Activity, Fragment, ViewModel, V ## Case: Official Android and Kotlin Libraries -Keep rules targeting official library packages like AndroidX, Kotlin, and Kotlinx are redundant as they are bundled within the libraries themselves. Manual rules are often broader than what is strictly needed. +Keep rules targeting official library packages like AndroidX, Kotlin, and +Kotlinx are redundant as they are bundled within the libraries themselves. +Manual rules are often broader than what is strictly needed. -**Common Mistakes:** `proguard -keep class androidx.** { *; } -keep class kotlinx.** { *; } -keep class kotlin.** { *; }` +**Common Mistakes:** +`proguard +-keep class androidx.** { *; } +-keep class kotlinx.** { *; } +-keep class kotlin.** { *; }` -**The Fix:** Delete these manual rules. Rely on the consumer keep rules packaged within these dependencies. +**The Fix:** Delete these manual rules. Rely on the consumer keep rules packaged +within these dependencies. *** ** * ** *** @@ -34,14 +57,18 @@ Keep rules targeting official library packages like AndroidX, Kotlin, and Kotlin ### Overly Broad Data Model Rules -The most common mistake is keeping entire packages of data models (POJOs/DTOs), keeping data models at all for deserialization is unnecessary. +The most common mistake is keeping entire packages of data models (POJOs/DTOs), +keeping data models at all for deserialization is unnecessary. -keep class com.example.app.models.** { *; } -keep class com.example.app.package.models.* { *; } ### Redundant Interface \& Adapter Rules -These rules added for TypeAdapter are unnecessary and are already covered by the library, and prevent R8 from effectively shrinking and optimizing custom adapters. R8 can determine if the adapter implementation are used. Keeping them globally prevents the removal of unused adapter implementations. +These rules added for TypeAdapter are unnecessary and are already covered by +the library, and prevent R8 from effectively shrinking and optimizing custom +adapters. R8 can determine if the adapter implementation are used. Keeping them +globally prevents the removal of unused adapter implementations. -keep class * extends com.google.gson.TypeAdapter -keep class * implements com.google.gson.TypeAdapterFactory @@ -50,7 +77,8 @@ These rules added for TypeAdapter are unnecessary and are already covered by the ### Unnecessary TypeToken Rules -There is no need to handle generic type erasure, Gson's own rules handle the necessary `TypeToken` preservation. +There is no need to handle generic type erasure, Gson's own rules handle the +necessary `TypeToken` preservation. -keep class com.google.gson.reflect.TypeToken { *; } -keep class * extends com.google.gson.reflect.TypeToken @@ -58,7 +86,8 @@ There is no need to handle generic type erasure, Gson's own rules handle the nec ### Internal and Example Packages -Keeping internal library logic prevents the compiler from stripping away dead code within the library. +Keeping internal library logic prevents the compiler from stripping away dead +code within the library. -keep class com.google.gson.internal.** { *; } -keep class com.google.gson.internal.reflect.** { *; } @@ -72,17 +101,22 @@ Keeping internal library logic prevents the compiler from stripping away dead co **The Fix:** 1. Use `@SerializedName` on every field in your data classes uses so that the field is retained after R8 optimization -2. Modern Gson (**v2.11.0+** ) bundles its own rules ([View Gson's embedded ProGuard rules](https://github.com/google/gson/blob/main/gson/src/main/resources/META-INF/proguard/gson.pro)). The bundled keep rules retains the `@SerializedName` annotated fields. If you are on an older version, move towards Gson version 2.11 because it has the necessary keep rules and delete the keep rules that target the classes used for gson serialization and deserialization +2. Modern Gson (**v2.11.0+** ) bundles its own rules ([View Gson's embedded + ProGuard + rules](https://github.com/google/gson/blob/main/gson/src/main/resources/META-INF/proguard/gson.pro)). The bundled keep rules retains the `@SerializedName` annotated fields. If you are on an older version, move towards Gson version 2.11 because it has the necessary keep rules and delete the keep rules that target the classes used for gson serialization and deserialization *** ** * ** *** ## Case: Retrofit -Retrofit has shipped with its own consumer keep rules from 2.9.0 and higher, so any keep rules for the library or classes depending on Retrofit is detrimental to the optimization process. +Retrofit has shipped with its own consumer keep rules from 2.9.0 and higher, so +any keep rules for the library or classes depending on Retrofit is detrimental +to the optimization process. ### Blanket Library Preservation -This is the most harmful Retrofit rule as it disables any shrinking for the entire library. +This is the most harmful Retrofit rule as it disables any shrinking for the +entire library. -keep class retrofit2.** { *; } -keep class retrofit2.api.** { *; } @@ -90,54 +124,70 @@ This is the most harmful Retrofit rule as it disables any shrinking for the enti ### Manual Annotation Keeps -Retrofit's consumer rules automatically keep the interfaces annotated with `@GET`, `@POST`, `@DELETE`, `@PUT`, `@HEAD`, `@OPTIONS`, `@PATCH`, making these manual rules obsolete. +Retrofit's consumer rules automatically keep the interfaces annotated with +`@GET`, `@POST`, `@DELETE`, `@PUT`, `@HEAD`, `@OPTIONS`, `@PATCH`, making these +manual rules obsolete. `-keepclasseswithmembers class * { @retrofit2.http.* ; }` ### Redundant Network Response and Adapter Rules -Network responses and third-party adapter wrappers (like RxJava) are often overly preserved by developers out of caution. +Network responses and third-party adapter wrappers (like RxJava) are often +overly preserved by developers out of caution. -keep,allowobfuscation,allowshrinking class retrofit2.Response -keep class retrofit2.adapter.rxjava2.Result { *; } -Fix: Verify you are using Retrofit 2.9.0 and higher. Retrofit from 2.9.0 bundles rules that detect its own HTTP annotations (@GET, @POST) ([View Retrofit's embedded ProGuard rules](https://github.com/square/retrofit/blob/master/retrofit/src/main/resources/META-INF/proguard/retrofit2.pro)). It will automatically keep the method signatures it needs to work. +Fix: Verify you are using Retrofit 2.9.0 and higher. Retrofit from 2.9.0 bundles +rules that detect its own HTTP annotations (@GET, @POST) ([View Retrofit's +embedded ProGuard +rules](https://github.com/square/retrofit/blob/master/retrofit/src/main/resources/META-INF/proguard/retrofit2.pro)). +It will automatically keep the method signatures it needs to work. *** ** * ** *** ## Case: Kotlin Coroutines -Kotlin Coroutines comes heavily optimized out of the box with embedded R8 rules (`kotlinx-coroutines-core` includes its own rules). +Kotlin Coroutines comes heavily optimized out of the box with embedded R8 rules +(`kotlinx-coroutines-core` includes its own rules). ### Blanket Coroutine Library Rules -Keeping everything under `kotlinx.coroutines` is extremely detrimental to app size, as coroutines contain a vast amount of internal APIs that aren't used. +Keeping everything under `kotlinx.coroutines` is extremely detrimental to app +size, as coroutines contain a vast amount of internal APIs that aren't used. `-keepclassmembers class kotlinx.coroutines.** { *; }` ### Redundant Internal Continuations -These low-level coroutine elements are preserved safely by the library's own consumer rules. Manually adding these prevents R8 from performing internal optimizations (such as removing unused continuations or inlining). +These low-level coroutine elements are preserved safely by the library's own +consumer rules. Manually adding these prevents R8 from performing internal +optimizations (such as removing unused continuations or inlining). -keepclassmembers class kotlin.coroutines.SafeContinuation { *; } -keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation ### Dispatcher and Exception Handler Rules -Sometimes developers notice crashes related to Missing Classes on old Android versions and add these rules, but if you are using an up-to-date version of Coroutines, these are handled automatically or are not an issue. +Sometimes developers notice crashes related to Missing Classes on old Android +versions and add these rules, but if you are using an up-to-date version of +Coroutines, these are handled automatically or are not an issue. -keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {} -keepnames class kotlinx.coroutines.CoroutineExceptionHandler {} -keepnames class kotlinx.coroutines.android.AndroidExceptionPreHandler {} -keepnames class kotlinx.coroutines.android.AndroidDispatcherFactory {} -**Fix** Remove any broad `kotlinx` keep rules. Coroutines (**v1.7.0+** ) bundle the necessary keep rules ([View Coroutines' embedded ProGuard rules](https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-core/jvm/resources/META-INF/proguard/coroutines.pro)). +**Fix** Remove any broad `kotlinx` keep rules. Coroutines (**v1.7.0+** ) bundle +the necessary keep rules ([View Coroutines' embedded ProGuard +rules](https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-core/jvm/resources/META-INF/proguard/coroutines.pro)). *** ** * ** *** ## Case: Parcelable -**Common Mistakes:** Legacy projects often contain `-keep class * implements android.os.Parcelable { public static final android.os.Parcelable$Creator *; }`. +**Common Mistakes:** Legacy projects often contain `-keep class * implements +android.os.Parcelable { public static final android.os.Parcelable$Creator *; }`. **The Fix:** @@ -151,12 +201,15 @@ Sometimes developers notice crashes related to Missing Classes on old Android ve ## Case: Room Database -**Common Mistakes:** Keeping DAO interfaces or the generated `_Impl` classes manually. +**Common Mistakes:** Keeping DAO interfaces or the generated `_Impl` classes +manually. -keep class * extends androidx.room.RoomDatabase -keep class *_*Impl { *; } -**The Fix:** Room generates its own ProGuard rules for the code it creates. Manual rules are redundant and prevent R8 from optimizing the database access layers. +**The Fix:** Room generates its own ProGuard rules for the code it creates. +Manual rules are redundant and prevent R8 from optimizing the database access +layers. - **Ideal Rule:** **None.** Delete all manual Room or DAO keeps. @@ -164,4 +217,6 @@ Sometimes developers notice crashes related to Missing Classes on old Android ve ## Summary -If you have updated your libraries to the versions mentioned, your `proguard-rules.pro` must not contain any keep rules for the libraries mentioned here. \ No newline at end of file +If you have updated your libraries to the versions mentioned, your +`proguard-rules.pro` must not contain any keep rules for the libraries +mentioned here. \ No newline at end of file diff --git a/performance/r8-analyzer/references/REFLECTION-GUIDE.md b/performance/r8-analyzer/references/REFLECTION-GUIDE.md index dd1cca5..8f23679 100644 --- a/performance/r8-analyzer/references/REFLECTION-GUIDE.md +++ b/performance/r8-analyzer/references/REFLECTION-GUIDE.md @@ -1,81 +1,139 @@ -A categorized summary of the keep rule examples, including the code patterns to look for (imports/usage) and the corresponding suggested rules. +A categorized summary of the keep rule examples, including the code patterns to +look for (imports/usage) and the corresponding suggested rules. ### 1. Reflection: Classes Loaded by Name **Scenario:** A library or app loads a class dynamically using a string name -- **Look for:** `Class.forName("...")`, `getDeclaredConstructor().newInstance()`, or interfaces used for dynamic loading. +- **Look for:** + `Class.forName("...")`, + `getDeclaredConstructor().newInstance()`, or interfaces used for dynamic loading. -- **Example Code:** `kotlin val taskClass = Class.forName(className) val task = taskClass.getDeclaredConstructor().newInstance() as StartupTask` +- **Example Code:** + `kotlin + val taskClass = Class.forName(className) + val task = taskClass.getDeclaredConstructor().newInstance() as StartupTask` -- **Suggested Keep Rule:** \`\`\`proguard +- **Suggested Keep Rule:** + \`\`\`proguard - -keep class \* implements com.example.library.StartupTask { (); } \`\`\` + -keep class \* implements com.example.library.StartupTask { + (); } \`\`\` ### 2. Reflection: Classes Passed using `::class.java` **Scenario:** An app passes a class reference directly to a library function. - **Look for:** `::class.java` (Kotlin) or `.class` (Java) passed as an argument. -- **Example Code:** `kotlin fun register(clazz: Class) { } // Usage: register(MyService::class.java)` +- **Example Code:** + `kotlin + fun register(clazz: Class) { } + // Usage: + register(MyService::class.java)` -- **Suggested Keep Rule:** \`\`\`proguard +- **Suggested Keep Rule:** + \`\`\`proguard # Keep the class itself (R8 usually handles this, but explicit rules ensure stability) - -keep class com.example.app.MyService { (); } \`\`\` + -keep class com.example.app.MyService { + (); } \`\`\` ### 3. Annotation-Based Reflection (Methods/Classes) -**Scenario:** Using custom annotations to mark methods or classes for reflective execution. +**Scenario:** Using custom annotations to mark methods or classes for reflective +execution. -**Look for:** Custom `@interface` definitions and `getDeclaredMethods()` filtered by annotation. **Example Code:** `kotlin annotation class ReflectiveExecutor // Logic: find methods annotated with @ReflectiveExecutor and invoke them` +**Look for:** Custom `@interface` definitions and `getDeclaredMethods()` +filtered by annotation. +**Example Code:** +`kotlin +annotation class ReflectiveExecutor +// Logic: find methods annotated with @ReflectiveExecutor and invoke them` - **Suggested Keep Rule:** \`\`\`proguard # Keep the annotation itself -keep @interface com.example.library.ReflectiveExecutor -# Keep members of any class annotated with this specific annotation -keepclassmembers class \* { @com.example.library.ReflectiveExecutor \*; } \`\`\` +# Keep members of any class annotated with this specific annotation +-keepclassmembers class \* { +@com.example.library.ReflectiveExecutor \*; +} +\`\`\` ### 4. Optional Dependencies (Soft Dependencies) -**Scenario:** A core library checks if an optional module is present in the classpath. +**Scenario:** A core library checks if an optional module is present in the +classpath. - **Look for:** `try-catch` blocks around `Class.forName()` used to toggle features. - **Example Code:** \`\`\`kotlin private const val VIDEO_TRACKER_CLASS = "com.example.analytics.video.VideoEventTracker" -try { Class.forName(VIDEO_TRACKER_CLASS).getDeclaredConstructor().newInstance() } catch (e: ClassNotFoundException) { /\* skip feature \*/ } \`\`\` +try { +Class.forName(VIDEO_TRACKER_CLASS).getDeclaredConstructor().newInstance() +} catch (e: ClassNotFoundException) { /\* skip feature \*/ } +\`\`\` -- **Suggested Keep Rule:** `proguard # Preserve the optional class so the check doesn't fail due to shrinking -keep class com.example.analytics.video.VideoEventTracker { (); }` +- **Suggested Keep Rule:** `proguard + # Preserve the optional class so the check doesn't fail due to shrinking + -keep class com.example.analytics.video.VideoEventTracker { + (); + }` ### 5. Accessing Private Members -**Scenario:** Using reflection to access internal fields or methods not exposed with public APIs. +**Scenario:** Using reflection to access internal fields or methods not exposed +with public APIs. - **Look for:** `getDeclaredField("...")` or `getDeclaredMethod("...")` followed by `isAccessible = true`. -- **Example Code:** `kotlin val secretField = instance::class.java.getDeclaredField("secretMessage") secretField.isAccessible = true` +- **Example Code:** + `kotlin + val secretField = instance::class.java.getDeclaredField("secretMessage") + secretField.isAccessible = true` -- **Suggested Keep Rule:** \`\`\`proguard +- **Suggested Keep Rule:** + \`\`\`proguard # Specifically keep the private field/method by name and type - -keepclassmembers class com.example.LibraryClass { private java.lang.String secretMessage; } \`\`\` + -keepclassmembers class com.example.LibraryClass { + private java.lang.String secretMessage; + } + \`\`\` ### 6. Parcelable (Manual Implementation) -**Scenario:** Implementing `Parcelable` without using the `@Parcelize` annotation. +**Scenario:** Implementing `Parcelable` without using the `@Parcelize` +annotation. - **Look for:** `implements Parcelable` and a static `CREATOR` field. -- **Example Code:** `kotlin class MyData : Parcelable { // Manual implementation with CREATOR field }` +- **Example Code:** + `kotlin + class MyData : Parcelable { + // Manual implementation with CREATOR field + }` -- **Suggested Keep Rule:** *(Note: If using `import kotlinx.parcelize.Parcelize`, R8/ProGuard rules are generated automatically. If manual, use the following:)* `proguard -keepclassmembers class * implements android.os.Parcelable { static android.os.Parcelable$Creator CREATOR; }` +- **Suggested Keep Rule:** + *(Note: If using `import kotlinx.parcelize.Parcelize`, R8/ProGuard rules are + generated automatically. If manual, use the following:)* + `proguard + -keepclassmembers class * implements android.os.Parcelable { + static android.os.Parcelable$Creator CREATOR; + }` ### 7. Enums and Obfuscation -**Scenario:** App uses `Enum.valueOf("STRING_NAME")` indirectly (e.g.,using JSON deserialization) and the enum names get obfuscated. +**Scenario:** App uses `Enum.valueOf("STRING_NAME")` indirectly (e.g.,using JSON +deserialization) and the enum names get obfuscated. - **Look for:** Unnecessary generic Enum keep rules in ProGuard files. -- **Example Code:** \`\`\`proguard +- **Example Code:** + \`\`\`proguard # Unnecessary rule - -keepclassmembers enum \* { \*; } \`\`\` -- **Suggested Keep Rule:** \*(Note: The default `proguard-android-optimize.txt` already contains the optimal rules for Enums (keeping `values()` and `valueOf(String)`). Any additional manual rules for Enums are redundant.) # No manual rule needed. Use default proguard-android-optimize.txt. \ No newline at end of file + -keepclassmembers enum \* { \*; } + \`\`\` +- **Suggested Keep Rule:** + \*(Note: The default `proguard-android-optimize.txt` already contains the optimal + rules for Enums (keeping `values()` and `valueOf(String)`). Any additional + manual rules for Enums are redundant.) # No manual rule needed. Use default + proguard-android-optimize.txt. \ No newline at end of file diff --git a/performance/r8-analyzer/references/REPORT_FORMAT.md b/performance/r8-analyzer/references/REPORT_FORMAT.md index 3527b6d..629c777 100644 --- a/performance/r8-analyzer/references/REPORT_FORMAT.md +++ b/performance/r8-analyzer/references/REPORT_FORMAT.md @@ -11,7 +11,8 @@ - \[Rule\]: Disables R8 globally. **Action**: Remove. -If there is -dontobfuscate, -dontoptimize or -dontshrink in the codebase, mention in this section +If there is -dontobfuscate, -dontoptimize or -dontshrink in the codebase, +mention in this section ## 3. Optimization summary @@ -19,7 +20,8 @@ If there is -dontobfuscate, -dontoptimize or -dontshrink in the codebase, mentio - **Shrinking score**: \[X\]% of code will be optimized by R8 by removing unused classes, fields and methods. \[100-X\]% of codebase contains redundant classes, fields and methods that can't be removed by R8. - **Obfuscation score**: \[X\]% of the codebase is available for R8 to obfuscate. -Increasing these scores increases the codebase available to R8 for optimizations. +Increasing these scores increases the codebase available to R8 for +optimizations. ## 4. Keep rules evaluation @@ -40,4 +42,10 @@ Increasing these scores increases the codebase available to R8 for optimizations ## 6. Historical analysis summary -*(Only include this section if a previous report existed. Summarize the changes in optimization scores here to track progress. For example:)* The previous app had scores: Optimization (XX%), Obfuscation (XX%), and Shrinking (XX%). The current app has scores: Optimization (YY%), Obfuscation (YY%), and Shrinking (YY%). **Change**: Optimization improved by ZZ%, Obfuscation improved by ZZ%, and Shrinking improved by ZZ%. \ No newline at end of file +*(Only include this section if a previous report existed. Summarize the changes +in optimization scores here to track progress. For example:)* The previous app +had scores: Optimization (XX%), Obfuscation (XX%), and Shrinking (XX%). The +current app has scores: Optimization (YY%), Obfuscation (YY%), and Shrinking +(YY%). +**Change**: Optimization improved by ZZ%, Obfuscation improved by ZZ%, and +Shrinking improved by ZZ%. \ No newline at end of file diff --git a/performance/r8-analyzer/references/android/topic/performance/app-optimization/enable-app-optimization.md b/performance/r8-analyzer/references/android/topic/performance/app-optimization/enable-app-optimization.md index 95c82e3..b5ddc30 100644 --- a/performance/r8-analyzer/references/android/topic/performance/app-optimization/enable-app-optimization.md +++ b/performance/r8-analyzer/references/android/topic/performance/app-optimization/enable-app-optimization.md @@ -1,5 +1,3 @@ -
- ## Android skills [View on GitHub](https://github.com/android/skills/tree/main/performance/r8-analyzer) @@ -12,7 +10,10 @@ Use the R8 Analyzer [Android skill](https://developer.android.com/tools/agents/a
-For the best user experience, you should optimize your app to make it as small and fast as possible. Our app optimizer, called R8, streamlines your app by removing unused code and resources, rewriting code to optimize runtime performance, and more. To your users, this means: +For the best user experience, you should optimize your app to make it as small +and fast as possible. Our app optimizer, called R8, streamlines your app by +removing unused code and resources, rewriting code to optimize runtime +performance, and more. To your users, this means: - Faster startup time - Reduced memory usage @@ -20,27 +21,46 @@ For the best user experience, you should optimize your app to make it as small a - Fewer [ANRs](https://developer.android.com/topic/performance/anrs/keep-your-app-responsive) > [!IMPORTANT] -> **Important:** You should always enable optimization for your app's release build; however, you probably don't want to enable it for tests or libraries. For more information about using R8 with tests, see [Test and troubleshoot the optimization](https://developer.android.com/topic/performance/app-optimization/test-and-troubleshoot-the-optimization). For more information about enabling R8 from libraries, see [Optimization for library authors](https://developer.android.com/topic/performance/app-optimization/library-optimization). +> **Important:** You should always enable optimization for your app's release build; however, you probably don't want to enable it for tests or libraries. For more information about using R8 with tests, see [Test and troubleshoot the +> optimization](https://developer.android.com/topic/performance/app-optimization/test-and-troubleshoot-the-optimization). For more information about enabling R8 from libraries, see [Optimization for library authors](https://developer.android.com/topic/performance/app-optimization/library-optimization). ## R8 optimization overview -R8 uses a multi-phase process to optimize your app for size and speed. Key operations include the following: +R8 uses a multi-phase process to optimize your app for size and speed. Key +operations include the following: -- **Code shrinking (also known as tree shaking)** : R8 identifies and removes unreachable code from your application and its library dependencies. By analyzing the entry points of your app (such as `Activities` or `Services` defined in the manifest), R8 builds a graph of referenced code and removes anything that remains unreferenced. +- **Code shrinking (also known as tree shaking)** : R8 identifies and removes + unreachable code from your application and its library dependencies. By + analyzing the entry points of your app (such as `Activities` or `Services` + defined in the manifest), R8 builds a graph of referenced code and removes + anything that remains unreferenced. -- **Logical optimizations**: R8 rewrites your code to improve execution efficiency and reduce overhead. Key techniques include: +- **Logical optimizations**: R8 rewrites your code to improve execution + efficiency and reduce overhead. Key techniques include: - - **Method inlining**: R8 replaces a method call site with the actual body of the called method. This eliminates the overhead of a function call and lets R8 conduct further optimizations. + - **Method inlining**: R8 replaces a method call site with the actual body + of the called method. This eliminates the overhead of a function call + and lets R8 conduct further optimizations. - - **Class merging**: R8 combines sets of classes and interfaces into a single class. This reduces the number of classes in the app, lowering memory pressure and improving startup speed. + - **Class merging**: R8 combines sets of classes and interfaces into a + single class. This reduces the number of classes in the app, lowering + memory pressure and improving startup speed. -- **Obfuscation (also known as minification)** : To reduce the size of the DEX file, R8 shortens the names of classes, fields, and methods (for example, `com.example.MyActivity` could become `a.b.a`). +- **Obfuscation (also known as minification)** : To reduce the size of the DEX + file, R8 shortens the names of classes, fields, and methods (for example, + `com.example.MyActivity` could become `a.b.a`). -Since 8.12.0 version of Android Gradle Plugin (AGP), R8 also optimizes resources as part of its optimization phases. For more information, see [Optimized resource shrinking](https://developer.android.com/topic/performance/app-optimization/enable-app-optimization#optimize-resource-shrinking). +Since 8.12.0 version of Android Gradle Plugin (AGP), R8 also optimizes resources +as part of its optimization phases. For more information, see [Optimized +resource shrinking](https://developer.android.com/topic/performance/app-optimization/enable-app-optimization#optimize-resource-shrinking). ## Enable optimization -To enable app optimization for your app use the appropriate DSL based on your project's AGP version. We recommend enabling app optimization only in the final version of your app that you test before publishing---usually your release build---because the optimizations increase the build time of your project and can make debugging harder due to the way it modifies code. +To enable app optimization for your app use the appropriate DSL based on your +project's AGP version. We recommend enabling app optimization only in the final +version of your app that you test before publishing---usually your release +build---because the optimizations increase the build time of your project and can +make debugging harder due to the way it modifies code. ### For AGP versions 9.3 and higher @@ -50,7 +70,8 @@ To enable app optimization for AGP 9.3 and higher: - Add your [keep rules](https://developer.android.com/topic/performance/app-optimization/add-keep-rules) to a file with the suffix `.keep` in the `src//keepRules` source set. For example, `src/main/keepRules/custom-rules.keep`. > [!NOTE] -> **Note:** The updated DSL enables both code and resource optimization. It also includes a set of default keep rules for the Android platform, equivalent to "proguard-android-optimize.txt". To omit the default rules, see [Omit default rules](https://developer.android.com/topic/performance/app-optimization/keep-rules-overview#omit-default). +> **Note:** The updated DSL enables both code and resource optimization. It also includes a set of default keep rules for the Android platform, equivalent to "proguard-android-optimize.txt". To omit the default rules, see [Omit default +> rules](https://developer.android.com/topic/performance/app-optimization/keep-rules-overview#omit-default). ### Kotlin @@ -78,7 +99,10 @@ To enable app optimization for AGP 9.3 and higher: ### Legacy DSL for AGP versions lower than 9.3 -To enable app optimization, set `isMinifyEnabled = true` (for code optimization) and `isShrinkResources = true` (for resource optimization) in your [release build's](https://developer.android.com/studio/publish/preparing#turn-off-debugging) app-level build script. We recommend that you always enable both settings. +To enable app optimization, set `isMinifyEnabled = true` (for code optimization) +and `isShrinkResources = true` (for resource optimization) in your [release +build's](https://developer.android.com/studio/publish/preparing#turn-off-debugging) app-level build script. We recommend that you always enable both +settings. ### Legacy DSL (Kotlin) @@ -126,7 +150,9 @@ To enable app optimization, set `isMinifyEnabled = true` (for code optimization) ## Improve R8 optimization -The performance benefits of R8 are directly correlated to how much of your codebase R8 is able to optimize. To get the maximum benefits out of R8, follow best practices: +The performance benefits of R8 are directly correlated to how much of your +codebase R8 is able to optimize. To get the maximum benefits out of R8, follow +best practices: - Enable R8 in [full mode](https://developer.android.com/topic/performance/app-optimization/full-mode) - Enable [obfuscation, optimization, and shrinking](https://developer.android.com/topic/performance/app-optimization/adopt-optimizations-incrementally) @@ -141,51 +167,87 @@ The R8 Configuration Analyzer lets you do the following: - Find the broadest keep rules - those which prevent the most optimization - and understand what optimization they prevent to refine them. -The R8 Configuration Analyzer is available in AGP version 9.3.0-alpha05 or from R8 version 9.3.7-dev. For more information, see [Analyze R8 configuration](https://developer.android.com/topic/performance/app-optimization/r8-configuration-analyzer). +The R8 Configuration Analyzer is available in AGP version 9.3.0-alpha05 or from +R8 version 9.3.7-dev. For more information, see [Analyze R8 configuration](https://developer.android.com/topic/performance/app-optimization/r8-configuration-analyzer). ## Optimize resource shrinking for even smaller apps -The 8.12.0 version of AGP introduces optimized resource shrinking, which aims to integrate resource and code optimization to create even smaller and faster apps. Before optimized resource shrinking, Android Asset Packaging Tool (AAPT2) generated keep rules that effectively treating resource shrinking separately from code, often retaining inaccessible code or resources that referenced each other. +The 8.12.0 version of AGP introduces optimized resource shrinking, which aims to +integrate resource and code optimization to create even smaller and faster apps. +Before optimized resource shrinking, Android Asset Packaging Tool (AAPT2) +generated keep rules that effectively treating resource shrinking separately +from code, often retaining inaccessible code or resources that referenced each +other. -With optimized resource shrinking, resources are considered like a part of program code, forming the reference graph. When a collection of code or resources is not referenced, it is not protected by a keep rule, and can be removed. +With optimized resource shrinking, resources are considered like a part of +program code, forming the reference graph. When a collection of code or +resources is not referenced, it is not protected by a keep rule, and can be +removed. ### Enable optimized resource shrinking -To enable the new optimized resource shrinking pipeline for AGP 8.12 or 8.13, add the following to your project's `gradle.properties` file: +To enable the new optimized resource shrinking pipeline for AGP 8.12 or 8.13, +add the following to your project's `gradle.properties` file: -If you are using AGP 9.0.0 or a newer version, you don't need to set `android.r8.optimizedResourceShrinking=true`. Optimized resource shrinking is automatically applied when `isShrinkResources = true` is enabled in your build configuration. + android.r8.optimizedResourceShrinking=true + +If you are using AGP 9.0.0 or a newer version, you don't need to set +`android.r8.optimizedResourceShrinking=true`. Optimized resource shrinking is +automatically applied when `isShrinkResources = true` is enabled in your build +configuration. ## Verify and configure R8 optimization settings -To enable R8 to use its [full optimization capabilities](https://developer.android.com/topic/performance/app-optimization/full-mode), remove the following line from your project's `gradle.properties` file, if it exists: +To enable R8 to use its [full optimization capabilities](https://developer.android.com/topic/performance/app-optimization/full-mode), remove the +following line from your project's `gradle.properties` file, if it exists: -`bash android.enableR8.fullMode=false # Remove this line from your codebase.` + android.enableR8.fullMode=false -Note that enabling app optimization makes stack traces difficult to understand, especially if R8 renames class or method names. To get stack traces that correctly correspond to your source code, see [Recover the original stack trace](https://developer.android.com/topic/performance/app-optimization/test-and-troubleshoot-the-optimization#recover-original-stack-trace). +Note that enabling app optimization makes stack traces difficult to understand, +especially if R8 renames class or method names. To get stack traces that +correctly correspond to your source code, see [Recover the original stack +trace](https://developer.android.com/topic/performance/app-optimization/test-and-troubleshoot-the-optimization#recover-original-stack-trace). -If R8 is enabled, you should also [create Startup Profiles](https://developer.android.com/topic/performance/baselineprofiles/dex-layout-optimizations) for even better startup performance. +If R8 is enabled, you should also [create Startup Profiles](https://developer.android.com/topic/performance/baselineprofiles/dex-layout-optimizations) for even better +startup performance. -If you enable app optimization and it causes errors, here are some strategies to fix them: +If you enable app optimization and it causes errors, here are some strategies to +fix them: - [Add keep rules](https://developer.android.com/topic/performance/app-optimization/add-keep-rules) to keep some code untouched. - [Adopt optimizations incrementally](https://developer.android.com/topic/performance/app-optimization/adopt-optimizations-incrementally). -- Update your code to [use libraries that are better suited for optimization](https://developer.android.com/topic/performance/app-optimization/choose-libraries-wisely). +- Update your code to [use libraries that are better suited for + optimization](https://developer.android.com/topic/performance/app-optimization/choose-libraries-wisely). > [!CAUTION] > **Caution:** Tools that replace or modify R8's output can negatively impact runtime performance. R8 is careful about including and testing many optimizations at the code level, in [DEX layout](https://developer.android.com/topic/performance/baselineprofiles/dex-layout-optimizations), and in correctly producing Baseline Profiles - other tools producing or modifying DEX files can break these optimizations, or otherwise regress performance. -If you are interested in optimizing your build speed, see [Configure how R8 runs](https://developer.android.com/build/r8-execution-profiles) for information on how to configure R8 based on your environment. +If you are interested in optimizing your build speed, see [Configure how R8 +runs](https://developer.android.com/build/r8-execution-profiles) for information on how to configure R8 based on your environment. ## AGP and R8 version behavior changes -The following table outlines the key features introduced in various versions of the Android Gradle Plugin (AGP) and the R8 compiler. +The following table outlines the key features introduced in various versions of +the Android Gradle Plugin (AGP) and the R8 compiler. | AGP version | Features introduced | |---|---| -| 9.3 | **Simplified DSL:** New \`optimization {}\` block enables requires less configuration- optimized resource shrinking is always enabled, default Android keep rules are opt-out. **\`keepRules\` source set:** If you use the new DSL, you must place your keep rules in the `src//keepRules` source set, in files with the suffix `.keep`. The source sets are also compatible with the legacy DSL. **KMP (Kotlin MultiPlatform) consumer rules:** KMP no longer requires setting `publish = true`, and supports the `src//keepRules` source set with the `.keep` extension. Note that the legacy DSL is still supported. | +| 9.3 | **Simplified DSL:** New \`optimization {}\` block enables requires less configuration- optimized resource shrinking is always enabled, default Android keep rules are opt-out.
**\`keepRules\` source set:** If you use the new DSL, you must place your keep rules in the `src//keepRules` source set, in files with the suffix `.keep`. The source sets are also compatible with the legacy DSL.
**KMP (Kotlin MultiPlatform) consumer rules:** KMP no longer requires setting `publish = true`, and supports the `src//keepRules` source set with the `.keep` extension.
Note that the legacy DSL is still supported. | | 9.1 | **Classes repackaged by default:** R8 repackages classes (moving them to the unnamed package, at the top level) to compact DEX further, eliminating the need to specify `-repackageclasses` option. For information about how this works and how to opt out, see [global options](https://developer.android.com/topic/performance/app-optimization/global-options#global-options). | | 9.0 | **Optimized resource shrinking:** Enabled by default (controlled using `android.r8.optimizedResourceShrinking`). [Optimized resource shrinking](https://developer.android.com/topic/performance/app-optimization/enable-app-optimization#optimize-resource-shrinking) helps integrate resource shrinking with the code optimization pipeline, leading to smaller, faster apps. By optimizing both code and resource references simultaneously, it identifies and removes resources referenced exclusively from unused code. This is a significant improvement over the previous separate optimization processes. This is especially useful for apps that share substantial resources and code across different form factor verticals, with measured improvements of over 50% in app size. The resulting size reduction leads to smaller downloads, faster installations, and a better user experience with faster startup, improved rendering, and fewer ANRs. **Library rule filtering:** Support for global options (for example, `-dontobfuscate`) in library consumer rules has been dropped, and apps will filter them out. For more information, see [Add global options](https://developer.android.com/topic/performance/app-optimization/global-options). **Kotlin null checks:** Optimized by default (controlled using `-processkotlinnullchecks`). This version also introduced significant improvements in build speed. For more information, see [Global options for additional optimization](https://developer.android.com/topic/performance/app-optimization/global-options#global-options). **Optimize specific packages:** You can use `packageScope` to optimize specific packages. This is in experimental support. For more information, see [Optimize specified packages with `packageScope`](https://developer.android.com/topic/performance/app-optimization/optimize-specified-packages). **Optimized by default:** Support for `getDefaultProguardFile("proguard-android.txt")` has been dropped, because it includes `-dontoptimize`, which should be avoided. Instead, use `"proguard-android-optimize.txt"`. If you need to globally disable optimization in your app, [add the flag manually to a proguard file](https://developer.android.com/topic/performance/app-optimization/global-options#global-options-2). | | 8.12 | **Optimized resource shrinking:** Initial support added (controlled using `android.r8.optimizedResourceShrinking`). [Optimized resource shrinking](https://developer.android.com/topic/performance/app-optimization/enable-app-optimization#optimize-resource-shrinking) helps integrate resource shrinking with the code optimization pipeline. You must manually enable it in this version of AGP. **Logcat retracing:** Support for automatic retracing in the Android Studio [Logcat window](https://developer.android.com/studio/debug/logcat). | | 8.6 | **Improved retracing:** Includes filename and line number retracing by default for all `minSdk` levels (previously required `minSdk` 26+ in version 8.2). Updating R8 helps ensure that stack traces from obfuscated builds are readily and clearly readable. This version improves how line numbers and source files are mapped, making it easier for tools like the Android Studio Logcat to automatically retrace crashes to the original source code. | | 8.0 | **Full mode by default:** [R8 full mode](https://developer.android.com/topic/performance/app-optimization/full-mode) provides significantly more powerful optimization. It is enabled by default. You can opt out using `android.enableR8.fullMode=false`. | -| 7.0 | **Full mode available:** Introduced as an opt-in feature using `android.enableR8.fullMode=true`. Full mode applies more powerful optimizations by making stricter assumptions about how your code uses reflection and other dynamic features. While it reduces app size and improves performance, it might require additional keep rules to prevent necessary code from being stripped. | \ No newline at end of file +| 7.0 | **Full mode available:** Introduced as an opt-in feature using `android.enableR8.fullMode=true`. Full mode applies more powerful optimizations by making stricter assumptions about how your code uses reflection and other dynamic features. While it reduces app size and improves performance, it might require additional keep rules to prevent necessary code from being stripped. | + +## Use R8 with other build systems + +While AGP is the recommended and officially supported build system for Android +apps, you might use an alternative build system like [Bazel](https://bazel.build/). If +you're using Bazel, you can integrate R8 into your build pipeline to shrink, +obfuscate, and optimize your app. + +For information about building an Android app using Bazel, see the [Android +Bazel tutorial](https://bazel.build/start/android-app) and the official [rules_android +repository](https://github.com/bazelbuild/rules_android). Note that Bazel isn't [officially supported](https://developer.android.com/build#other-build-systems) +for Android app development. \ No newline at end of file diff --git a/performance/r8-analyzer/references/android/training/testing/other-components/ui-automator.md b/performance/r8-analyzer/references/android/training/testing/other-components/ui-automator.md index 75d0d08..2b5b253 100644 --- a/performance/r8-analyzer/references/android/training/testing/other-components/ui-automator.md +++ b/performance/r8-analyzer/references/android/training/testing/other-components/ui-automator.md @@ -1,26 +1,34 @@ -The UI Automator testing framework provides a set of APIs to build UI tests that interact with user apps and system apps. +The UI Automator testing framework provides a set of APIs to build UI tests that +interact with user apps and system apps. > [!NOTE] > **Note:** This documentation covers the modern approach to writing UI Automator tests, introduced with [UI Automator 2.4](https://developer.android.com/jetpack/androidx/releases/test-uiautomator#2.4.0). This approach makes your tests more concise, readable, and robust. The API is under development, and we strongly recommend using it for any new development with UI Automator. The [legacy API guidance](https://developer.android.com/training/testing/other-components/ui-automator-legacy) is also available. ## Introduction to modern UI Automator testing -UI Automator 2.4 introduces a streamlined, Kotlin-friendly Domain Specific Language (DSL) that simplifies writing UI tests for Android. This new API surface focuses on predicate-based element finding and explicit control over app states. Use it to create more maintainable and reliable automated tests. +UI Automator 2.4 introduces a streamlined, Kotlin-friendly Domain Specific +Language (DSL) that simplifies writing UI tests for Android. This new API +surface focuses on predicate-based element finding and explicit control over app +states. Use it to create more maintainable and reliable automated tests. -UI Automator lets you test an app from outside of the app's process. This lets you test release versions with minification applied. UI Automator also helps when writing macrobenchmark tests. +UI Automator lets you test an app from outside of the app's process. This +lets you test release versions with minification applied. UI Automator also +helps when writing macrobenchmark tests. Key features of the modern approach include: - A dedicated `uiAutomator` test scope for cleaner and more expressive test code. - Methods like `onElement`, `onElements`, and `onElementOrNull` for finding UI elements with clear predicates. -- Built-in waiting mechanism for conditional elements `onElement*(timeoutMs: Long = 10000)` +- Built-in waiting mechanism for conditional elements `onElement*(timeoutMs: + Long = 10000)` - Explicit app state management such as `waitForStable` and `waitForAppToBeVisible`. - Direct interaction with accessibility window nodes for multi-window testing scenarios. - Built-in screenshot capabilities and a `ResultsReporter` for visual testing and debugging. ## Set up your project -To begin using the modern UI Automator APIs, update your project's `build.gradle.kts` file to include the [latest dependency](https://developer.android.com/jetpack/androidx/releases/test-uiautomator#2.4.0): +To begin using the modern UI Automator APIs, update your project's +`build.gradle.kts` file to include the [latest dependency](https://developer.android.com/jetpack/androidx/releases/test-uiautomator#2.4.0): ### Kotlin @@ -42,7 +50,9 @@ The following sections describe core concepts of the modern UI Automator API. ### The uiAutomator test scope -Access all new UI Automator APIs within the **`uiAutomator { ... }`** block. This function creates a `UiAutomatorTestScope` that provides a concise and type-safe environment for your test operations. +Access all new UI Automator APIs within the **`uiAutomator { ... }`** +block. This function creates a `UiAutomatorTestScope` that provides a concise +and type-safe environment for your test operations. uiAutomator { // All your UI Automator actions go here @@ -52,9 +62,13 @@ Access all new UI Automator APIs within the **`uiAutomator { ... }`** block. Thi ### Find UI elements -Use UI Automator APIs with predicates to locate UI elements. These predicates let you define conditions for properties such as text, selected or focused state, and content description. +Use UI Automator APIs with predicates to locate UI elements. These predicates +let you define conditions for properties such as text, selected or focused +state, and content description. -- `onElement { predicate }`: Returns the first UI element that matches the predicate within a default timeout. The function throws an exception if it doesn't locate a matching element. +- `onElement { predicate }`: Returns the first UI element that matches the + predicate within a default timeout. The function throws an exception if it + doesn't locate a matching element. // Find a button with the text "Submit" and click it onElement { textAsString() == "Submit" }.click() @@ -67,12 +81,15 @@ Use UI Automator APIs with predicates to locate UI elements. These predicates le clickAllow() } -- `onElementOrNull { predicate }`: Similar to `onElement`, but returns `null` if the function finds no matching element within the timeout. It doesn't throw an exception. Use this method for optional elements. +- `onElementOrNull { predicate }`: Similar to `onElement`, but returns + `null` if the function finds no matching element within the timeout. It + doesn't throw an exception. Use this method for optional elements. val optionalButton = onElementOrNull { textAsString() == "Skip" } optionalButton?.click() // Click only if the button exists -- `onElements { predicate }`: Waits until at least one UI element matches the given predicate, then returns a list of all matching UI elements. +- `onElements { predicate }`: Waits until at least one UI element matches + the given predicate, then returns a list of all matching UI elements. // Get all items in a list Ui element val listItems = onElements { className == "android.widget.TextView" && isClickable } @@ -80,7 +97,9 @@ Use UI Automator APIs with predicates to locate UI elements. These predicates le Here are some tips for using `onElement` calls: -- Chain `onElement` calls for nested elements: You can chain `onElement` calls to find elements within other elements, following a parent-child hierarchy. +- Chain `onElement` calls for nested elements: You can chain `onElement` + calls to find elements within other elements, following a parent-child + hierarchy. // Find a parent Ui element with ID "first", then its child with ID "second", // then its grandchild with ID "third", and click it. @@ -89,7 +108,8 @@ Here are some tips for using `onElement` calls: .onElement { viewIdResourceName == "third" } .click() -- Specify a timeout for `onElement*` functions by passing a value representing milliseconds. +- Specify a timeout for `onElement*` functions by passing a value representing + milliseconds. // Find a Ui element with a zero timeout (instant check) onElement(0) { viewIdResourceName == "something" }.click() @@ -99,7 +119,8 @@ Here are some tips for using `onElement` calls: ### Interact with UI elements -Interact with UI elements by simulating clicks or setting text in editable fields. +Interact with UI elements by simulating clicks or setting text in editable +fields. // Click a Ui element onElement { textAsString() == "Tap Me" }.click() @@ -112,7 +133,8 @@ Interact with UI elements by simulating clicks or setting text in editable field ## Handle app states and watchers -Manage the lifecycle of your app and handle unexpected UI elements that might appear during your tests. +Manage the lifecycle of your app and handle unexpected UI elements that might +appear during your tests. ### App lifecycle management @@ -133,7 +155,9 @@ The APIs provide ways to control the state of the app under test: ### Handle unexpected UI -The `watchFor` API lets you define handlers for unexpected UI elements, such as permission dialogs, that might appear during your test flow. This uses the internal watcher mechanism but offers more flexibility. +The `watchFor` API lets you define handlers for unexpected UI elements, +such as permission dialogs, that might appear during your test flow. This +uses the internal watcher mechanism but offers more flexibility. import androidx.test.uiautomator.PermissionDialog @@ -156,19 +180,24 @@ The `watchFor` API lets you define handlers for unexpected UI elements, such as onElement { textAsString() == "Request Permissions" }.click() } -`PermissionDialog` is an example of a `ScopedWatcher`, where `T` is the object passed as a scope to the block in `watchFor`. You can create custom watchers based on this pattern. +`PermissionDialog` is an example of a `ScopedWatcher`, where `T` is the +object passed as a scope to the block in `watchFor`. You can create custom +watchers based on this pattern. ### Wait for app visibility and stability -Sometimes tests need to wait for elements to become visible or stable. UI Automator offers several APIs to help with this. +Sometimes tests need to wait for elements to become visible or stable. +UI Automator offers several APIs to help with this. -The `waitForAppToBeVisible("com.example.targetapp")` waits for a UI element with the given package name to appear on the screen within a customizable timeout. +The `waitForAppToBeVisible("com.example.targetapp")` waits for a UI element with +the given package name to appear on the screen within a customizable timeout. // Wait for the app to be visible after launching it startApp("com.example.targetapp") waitForAppToBeVisible("com.example.targetapp") -Use the `waitForStable()` API to verify that the app's UI is considered stable before interacting with it. +Use the `waitForStable()` API to verify that the app's UI is considered stable +before interacting with it. // Wait for the entire active window to become stable activeWindow().waitForStable() @@ -181,11 +210,26 @@ Use the `waitForStable()` API to verify that the app's UI is considered stable b ## Use UI Automator for Macrobenchmarks and Baseline Profiles -Use UI Automator for performance testing with [Jetpack Macrobenchmark](https://developer.android.com/topic/performance/benchmarking/macrobenchmark-overview) and for generating [Baseline Profiles](https://developer.android.com/topic/performance/baselineprofiles/overview), as it provides a reliable way to interact with your app and measure performance from an end-user perspective. +Use UI Automator for performance testing with [Jetpack Macrobenchmark](https://developer.android.com/topic/performance/benchmarking/macrobenchmark-overview) +and for generating [Baseline Profiles](https://developer.android.com/topic/performance/baselineprofiles/overview), as it provides a reliable way to +interact with your app and measure performance from an end-user perspective. -Macrobenchmark uses UI Automator APIs to drive the UI and measure interactions. For example, in startup benchmarks, you can use `onElement` to detect when UI content is fully loaded, enabling you to measure [Time to Full Display (TTFD)](https://developer.android.com/topic/performance/vitals/launch-time#time-full). In jank benchmarks, UI Automator APIs are used to scroll lists or run animations to measure frame timings. Functions like `startActivity()` or `startIntent()` are useful for getting the app into the correct state before measurement begins. +Macrobenchmark uses UI Automator APIs to drive the UI and measure interactions. +For example, in startup benchmarks, you can use `onElement` to detect when UI +content is fully loaded, enabling you to measure [Time to Full Display +(TTFD)](https://developer.android.com/topic/performance/vitals/launch-time#time-full). In jank benchmarks, UI Automator APIs are used to scroll lists or +run animations to measure frame timings. Functions like `startActivity()` or +`startIntent()` are useful for getting the app into the correct state before +measurement begins. -When [generating Baseline Profiles](https://developer.android.com/topic/performance/baselineprofiles/create-baselineprofile), you automate your app's critical user journeys (CUJs) to record which classes and methods require pre-compilation. UI Automator is an ideal tool for writing these automation scripts. The modern DSL's predicate-based element finding and built-in wait mechanisms (`onElement`) lead to more robust and deterministic test execution compared to other methods. This stability reduces flakiness and ensures that the generated Baseline Profile accurately reflects the code paths executed during your most important user flows. +When [generating Baseline Profiles](https://developer.android.com/topic/performance/baselineprofiles/create-baselineprofile), you automate your app's critical user +journeys (CUJs) to record which classes and methods require pre-compilation. UI +Automator is an ideal tool for writing these automation scripts. The modern +DSL's predicate-based element finding and built-in wait mechanisms (`onElement`) +lead to more robust and deterministic test execution compared to other methods. +This stability reduces flakiness and ensures that the generated Baseline Profile +accurately reflects the code paths executed during your most important user +flows. ## Advanced features @@ -193,7 +237,9 @@ The following features are useful for more complex testing scenarios. ### Interact with multiple windows -The UI Automator APIs let you directly interact with and inspect UI elements. This is particularly useful for scenarios involving multiple windows, such as Picture-in-Picture (PiP) mode or split-screen layouts. +The UI Automator APIs let you directly interact with and inspect UI +elements. This is particularly useful for scenarios involving multiple windows, +such as Picture-in-Picture (PiP) mode or split-screen layouts. // Find the first window that is in Picture-in-Picture mode val pipWindow = windows() @@ -204,7 +250,9 @@ The UI Automator APIs let you directly interact with and inspect UI elements. Th ### Screenshots and visual assertions -Capture screenshots of the entire screen, specific windows, or individual UI elements directly within your tests. This is helpful for visual regression testing and debugging. +Capture screenshots of the entire screen, specific windows, or +individual UI elements directly within your tests. This is helpful for visual +regression testing and debugging. uiautomator { // Take a screenshot of the entire active window @@ -222,11 +270,14 @@ Capture screenshots of the entire screen, specific windows, or individual UI ele pipWindowScreenshot.saveToFile(File("/sdcard/Download/pip_screenshot.png")) } -The `saveToFile` extension function for Bitmap simplifies saving the captured image to a specified path. +The `saveToFile` extension function for Bitmap simplifies saving the captured +image to a specified path. ### Use ResultsReporter for debugging -The `ResultsReporter` helps you associate test artifacts, like screenshots, directly with your test results in Android Studio for easier inspection and debugging. +The `ResultsReporter` helps you associate test artifacts, like screenshots, +directly with your test results in Android Studio for easier inspection and +debugging. uiAutomator { startApp("com.example.targetapp") @@ -248,7 +299,8 @@ The `ResultsReporter` helps you associate test artifacts, like screenshots, dire ## Migrate from older UI Automator versions -If you have existing UI Automator tests written with older API surfaces, use the following table as a reference to migrate to the modern approach: +If you have existing UI Automator tests written with older API surfaces, use the +following table as a reference to migrate to the modern approach: | Action type | Old UI Automator method | New UI Automator method | |---|---|---| diff --git a/play/engage-sdk-integration/SKILL.md b/play/engage-sdk-integration/SKILL.md index 9479934..9eddd7d 100644 --- a/play/engage-sdk-integration/SKILL.md +++ b/play/engage-sdk-integration/SKILL.md @@ -6,7 +6,7 @@ description: Helps developers integrate, debug, and resolve Play Engage SDK impl license: Complete terms in LICENSE.txt metadata: author: Google LLC - last-updated: '2026-08-06' + last-updated: '2026-08-19' keywords: - android - engage @@ -15,7 +15,9 @@ metadata: - google play --- -This skill guides you through integrating the Play Engage SDK into an Android app. It ensures that the code follows the mandatory structure and uses the required Engage entities for each vertical. +This skill guides you through integrating the Play Engage SDK into an Android +app. It ensures that the code follows the mandatory structure and uses the +required Engage entities for each vertical. ## Workflow @@ -52,7 +54,8 @@ Follow these steps to assist the developer: - Ask the developer to provide the source of actual data you'll publish. - Once you identify the data source, use it to fetch the data in the app's local model schema. - Use `{ENGAGE_CODE_DIR}/ItemToEntityConverter` to convert this data to an Engage entity. - - Use obtained Engage entity model data with `{ENGAGE_CODE_DIR}/ ClusterRequestFactory` to get cluster requests. + - Use obtained Engage entity model data with `{ENGAGE_CODE_DIR}/ + ClusterRequestFactory` to get cluster requests. - Call corresponding cluster publishing method obtained from `{VERTICAL}.md` in the **[references/schemas/](references/schemas)** directory with the obtained request in previous step in `{ENGAGE_CODE_DIR}/EngageWorker`. 5. **Gradle and manifest updates:** @@ -62,14 +65,20 @@ Follow these steps to assist the developer: - Provide the necessary `implementation` dependencies for `build.gradle` or `build.gradle.kts` from [patterns.md](references/patterns.md). - Provide the `` and `` declarations for `AndroidManifest.xml`. - Note: Except for TV, there aren't any vertical-specific imports. For all other verticals, `com.google.android.engage:engage-core:1.6.0` is sufficient. -6. **Debugging:** +6. **Debugging and verification**: + - **Self-verification checklist** : Before considering your work complete, you must verify that you've implemented all of the following: + - \[ \] Registered `EngageBroadcastReceiver` **statically** in `AndroidManifest.xml` (inside the `` tag). + - \[ \] Registered `EngageBroadcastReceiver` **dynamically** by calling `EngageBroadcastReceiver.register(context)` in the `Application` class or main `Activity` class. + - \[ \] Implemented the `register` method in `EngageBroadcastReceiver`'s `companion object` to handle dynamic registration. + - \[ \] Handled empty data lists in `EngageWorker` (for example, by deleting the cluster instead of publishing empty data). + - \[ \] Used `--no-daemon` for all Gradle compilations. - Perform a Gradle sync. - - If errors occur, follow this resolution order: - - Fix import errors. For package `com.google.android.engage` or classes starting with `AppEngage`, verify the package name in the `{VERTICAL}.md` in **[references/schemas/](references/schemas)** directory or [common.md](references/common.md). - - Fix any other errors. - - Execute a full Gradle build and resolve any remaining compilation issues. Repeat this step until the Gradle build is successful. -7. **User checklist:** At the end of code generation, notify the user to go through this checklist to verify that the integration is complete and as intended: + - If errors occur (such as import failures, namespace conflicts, or compile errors), read **[references/troubleshooting.md](references/troubleshooting.md)** to resolve them. + - Execute a Gradle compilation. You must run `./gradlew compileDebugUnitTestSources --no-daemon` or `./gradlew assembleDebug --no-daemon`. See **[references/troubleshooting.md](references/troubleshooting.md)** for compile rules and warnings about fast-compilation shortcuts. Repeat this step until compilation is successful. +7. **User checklist:** At the end of code generation, notify the user to go + through this checklist to verify that the integration is complete and as + intended: - \[ \] Verify that all the Engage-related files are created in `{ENGAGE_CODE_DIR}/`: - `Constants` @@ -87,7 +96,8 @@ Follow these steps to assist the developer: ## Reference materials -- **FAQ:** [Engage FAQ](references/android/guide/playcore/engage/faq.md) - Refer to this document for answers to frequently asked questions from developers. +- **FAQ:** [Engage FAQ](references/android/guide/playcore/engage/faq.md) - Refer to this document for answers to frequently + asked questions from developers. - **Vertical-specific guides:** diff --git a/play/engage-sdk-integration/references/android/guide/playcore/engage/faq.md b/play/engage-sdk-integration/references/android/guide/playcore/engage/faq.md index e0d1f4a..3ed6401 100644 --- a/play/engage-sdk-integration/references/android/guide/playcore/engage/faq.md +++ b/play/engage-sdk-integration/references/android/guide/playcore/engage/faq.md @@ -2,29 +2,47 @@ #### Who manages the content publishing job? -The app developer manages the content publishing job and sends requests to the Engage Service. In this way, developer partners have more control over when and how to publish content to the users. This avoids waking up the partner app too frequently to publish content. +The app developer manages the content publishing job and sends requests to the +Engage Service. In this way, developer partners have more control over when and +how to publish content to the users. This avoids waking up the partner app too +frequently to publish content. #### Does a developer need to publish all cluster types? -While technically developers are free to publish just one cluster, we **strongly advise** including more. Otherwise, developers miss the opportunity to drive better engagement with their content. We **highly recommend** publishing all cluster types for each vertical. +While technically developers are free to publish just one cluster, we **strongly +advise** including more. Otherwise, developers miss the opportunity to drive +better engagement with their content. We **highly recommend** publishing all +cluster types for each vertical. #### How often should the developer partner be publishing data using the work manager while the app is running? -This is to be decided by the developer partner. Google recommends publishing once or twice per day for general recommendation content, and to use an event-driven methodology for shopping cart, reorder, and other continuation content (for example, start the worker as a callback of the user adding items to the cart or the user stopping a movie halfway). For **social** apps, it's critical to publish updated recommendation clusters **after each app usage**. Social app users are more interested in the most recent recommendations and ideally would like to see a post at most once. +This is to be decided by the developer partner. Google recommends publishing +once or twice per day for general recommendation content, and to use an +event-driven methodology for shopping cart, reorder, and other continuation +content (for example, start the worker as a callback of the user adding items +to the cart or the user stopping a movie halfway). +For **social** apps, it's critical to publish updated recommendation clusters +**after each app usage**. Social app users are more interested in the most +recent recommendations and ideally would like to see a post at most once. #### When should the developer call delete APIs? -Delete APIs should only be called when there is no content to publish. **Don't** call delete and publish APIs subsequently to replace content; the publish APIs remove the earlier content automatically. +Delete APIs should only be called when there is no content to publish. **Don't** +call delete and publish APIs subsequently to replace content; the publish +APIs remove the earlier content automatically. ## Broadcast Intent FAQs #### Why do Android app developers need to register for broadcast intents? -In order to serve fresh content to the user, you should use broadcast intents to trigger a data sync in cases where users might not use the app frequently. +In order to serve fresh content to the user, you should use broadcast intents to +trigger a data sync in cases where users might not use the app frequently. #### Unable to test broadcast intent -The verification app doesn't support testing broadcast intent with permission. You have to remove permissions while testing and add them back before switching the SDK to prod version in [Step 6](https://developer.android.com/guide/playcore/engage/workflow#switch-to-prod). +The verification app doesn't support testing broadcast intent with +permission. You have to remove permissions while testing and +add them back before switching the SDK to prod version in [Step 6](https://developer.android.com/guide/playcore/engage/workflow#switch-to-prod). #### Background execution not allowed @@ -59,19 +77,27 @@ While integrating with the SDK, you may come across the following errors: #### Validation errors at the app, cluster, entity level -Summaries at the app, cluster, and entity levels display a count of validation errors. These errors correspond to missing required fields or invalid values provided. Error messages show up in red beneath each relevant field. Fix all validation errors and check for correctness before sharing the APK. +Summaries at the app, cluster, and entity levels display a count of +validation errors. These errors correspond to missing required fields or +invalid values provided. Error messages show up in red beneath each relevant +field. Fix all validation errors and check for correctness before sharing +the APK. #### Testing Deep links -The deep links are associated with the package name. A good way to test deep links is using the adb tool. +The deep links are associated with the package name. A good way to test +deep links is using the adb tool. adb shell am start -W -a android.intent.action.VIEW -d #### How can I calculate the impact of the integration? -The deep links are a great way to track the attribution. The deep link URLs that take users to your app can be included with additional tracking params. For example - "http://xx/deeplink?source_tag=engage". +The deep links are a great way to track the attribution. The deep link URLs +that take users to your app can be included with additional tracking params. +For example - "http://xx/deeplink?source_tag=engage". -Developers can add their own tracking params and provide attribution to calculate impact. +Developers can add their own tracking params and provide attribution to +calculate impact. ## Engage for TV 2.0 FAQs @@ -79,24 +105,33 @@ Developers can add their own tracking params and provide attribution to calculat #### What is Engage? -Engage takes the "pick up where you left off" experience to the next level! It's a significant upgrade that allows viewers to seamlessly resume their content across a wider range of devices. Imagine starting a movie on your Google TV and then effortlessly continuing it on your phone during your commute -- that's the power of Engage. +Engage takes the "pick up where you left off" experience to the next level! It's +a significant upgrade that allows viewers to seamlessly resume their content +across a wider range of devices. Imagine starting a movie on your Google TV and +then effortlessly continuing it on your phone during your commute -- that's the +power of Engage. -This new system is designed to boost viewer engagement and retention by providing a smooth, frictionless experience across the entire Google ecosystem. +This new system is designed to boost viewer engagement and retention by +providing a smooth, frictionless experience across the entire Google ecosystem. #### Is Video Discovery API the same as Engage? -Yes, they are the same. The Engage SDK is a library that includes support for the continue watching row. Engage supports more content entity types than video, which is why the integration is no longer named "Video Discovery". +Yes, they are the same. The Engage SDK is a library that includes support for +the continue watching row. Engage supports more content entity types than video, +which is why the integration is no longer named "Video Discovery". #### What are the benefits of using Engage? -Answer: Engage makes it easier than ever for viewers to pick up where they left off on your content, no matter what device they're using. Here's how it works: +Answer: Engage makes it easier than ever for viewers to pick up where they left +off on your content, no matter what device they're using. Here's how it works: - Seamless experience across Google: Start watching on your Google TV, and continue seamlessly on your Android phone, iPhone, or Android tablet. It even works on devices where you haven't installed the app yet! - Increased engagement and retention: Engage helps bring users back to your app, even on new devices. By letting users resume their favorite shows, you increase the chances they'll keep watching. - Wider reach: Beyond Google TV, Engage works across other Android media experiences, like Play Collections and other Google media apps. - Backward compatible: If you're already using the older "[Watch Next](https://developer.android.com/training/tv/discovery/watch-next-add-programs)" feature, no problem! Engage is backward compatible, so your existing integration will still work. -Important Note: All new Continue Watching integrations must use Engage. The older "Cross Device Play Next" system is being phased out. +Important Note: All new Continue Watching integrations must use Engage. The +older "Cross Device Play Next" system is being phased out. #### What surfaces support Engage? @@ -110,7 +145,8 @@ Important Note: All new Continue Watching integrations must use Engage. The olde #### Is Engage SDK for Continue Watching? -Yes, the Engage SDK supports content for the continue watching row. It is required to integrate with Engage. +Yes, the Engage SDK supports content for the continue watching row. It is +required to integrate with Engage. #### Is Engage available for everyone? @@ -119,53 +155,85 @@ Engage is being rolled out in phases. - Early Access: We're initially granting access to a select group of partners through an Early Access Program (EAP). - Expanding Access: We're working hard to make Engage available to all developers soon. -For a smooth and successful launch, we have safeguards in place to manage the rollout. This involves both an allowlist on the Engage side and a separate check within the Engage SDK. Whether you are an EAP partner or wants to be onboard soon, please contact us so that we can set up the access permissions before you begin with Engage SDK integration. +For a smooth and successful launch, we have safeguards in place to manage the +rollout. This involves both an allowlist on the Engage side and a separate check +within the Engage SDK. Whether you are an EAP partner or wants to be onboard +soon, please contact us so that we can set up the access permissions before you +begin with Engage SDK integration. #### Is there a recommended image size we should provide? -Image requirements have been updated in the [Create Entities](https://developer.android.com/guide/playcore/engage/watch#image-specs) section. +Image requirements have been updated in the [Create Entities](https://developer.android.com/guide/playcore/engage/watch#image-specs) +section. #### With this new API documentation, will the Continue Watching data pulled by the Google server from the client and will it be reflected in all the devices? -The new API offers significant improvements for content on the continue watching row, including: +The new API offers significant improvements for content on the continue watching +row, including: -- **Seamless experience across Google TVs:** Users can start watching on one Google TV and resume on any other Google TV logged in with the same account. This feature also works with older Android TV versions. +- **Seamless experience across Google TVs:** Users can start watching on one + Google TV and resume on any other Google TV logged in with the same account. + This feature also works with older Android TV versions. -- **Mobile app integration:** Content from Engage is surfaced on the Google TV mobile app for Android and iOS, allowing users to seamlessly switch between their TV and mobile devices. +- **Mobile app integration:** Content from Engage is surfaced on the Google TV + mobile app for Android and iOS, allowing users to seamlessly switch between + their TV and mobile devices. -- **Enhanced user retention:** Even on devices without the app installed or where the user isn't logged in, content in the continue watching row encourages users to re-engage with your app, boosting retention. +- **Enhanced user retention:** Even on devices without the app installed or + where the user isn't logged in, content in the continue watching row + encourages users to re-engage with your app, boosting retention. -- **Expansion to other platforms:** Engage extends to other Google media platforms like Android, Play Collections, tablets, and other Google media apps \& surfaces on Android, maximizing user engagement across devices. +- **Expansion to other platforms:** Engage extends to other Google media + platforms like Android, Play Collections, tablets, and other Google media + apps \& surfaces on Android, maximizing user engagement across devices. #### What is the limit on the number of entities I can publish to the Continuation cluster? -Each developer partner is limited to a maximum of 5 entities in the Continuation cluster. This limit is for fair distribution of content on the "continue watching" row on Google TV, which is a shared space for multiple media providers. +Each developer partner is limited to a maximum of 5 entities in the Continuation +cluster. This limit is for fair distribution of content on the "continue +watching" row on Google TV, which is a shared space for multiple media +providers. #### What happens if I try to publish more than 5 entities? -Engage SDK will reject your publish request if it exceeds the 5-entity limit. You'll need to reduce the number of entities in your request to successfully publish. You should only include the entities where the users have left off watching, so in most cases, there will be only a few such entities. When there are more than 5 such entities, you could choose the more recent ones to publish. +Engage SDK will reject your publish request if it exceeds the 5-entity limit. +You'll need to reduce the number of entities in your request to successfully +publish. You should only include the entities where the users have left off +watching, so in most cases, there will be only a few such entities. When there +are more than 5 such entities, you could choose the more recent ones to publish. #### Why is there a limit on the number of entities? -The continue watching row on Google TV displays content from various media providers. Limiting the number of entities per provider so that users see a diverse selection of content from all their favorite sources, promoting a fair and balanced user experience. +The continue watching row on Google TV displays content from various media +providers. Limiting the number of entities per provider so that users see a +diverse selection of content from all their favorite sources, promoting a fair +and balanced user experience. ### Verification App Questions #### Is it mandatory to test my app with the verification app before submission? -Yes, testing your app with the verification app is essential before submitting your APK. +Yes, testing your app with the verification app is essential before submitting +your APK. -While we understand you might be confident in your implementation, the Engage integration has many intricate components. The verification app acts as a safety net, catching potential issues early on and saving you valuable time and effort in the long run. +While we understand you might be confident in your implementation, the Engage +integration has many intricate components. The verification app acts as a safety +net, catching potential issues early on and saving you valuable time and effort +in the long run. -Think of it as a quick checkup that helps guarantee a smooth launch and a great user experience. +Think of it as a quick checkup that helps guarantee a smooth launch and a great +user experience. -By identifying and addressing any problems beforehand, you can avoid the frustration of rejections and resubmissions. +By identifying and addressing any problems beforehand, you can avoid the +frustration of rejections and resubmissions. -To submit your APK, you'll need to include a screenshot showing that your app has passed the verification process. +To submit your APK, you'll need to include a screenshot showing that your app +has passed the verification process. #### What are some common mistakes to watch out for during integration? -The verification app is designed to catch potential issues with your Engage integration. Here are some common mistakes that developers often encounter: +The verification app is designed to catch potential issues with your Engage +integration. Here are some common mistakes that developers often encounter: For all content types (movies, TV episodes, live streams, video clips): @@ -190,23 +258,29 @@ For Video Clips: - Creator Information: Specify the creator of the video clip. -Remember: The verification app will flag these issues, allowing you to fix them before submitting your app. This saves you time and ensures a smoother experience for your users. +Remember: The verification app will flag these issues, allowing you to fix them +before submitting your app. This saves you time and ensures a smoother +experience for your users. ### Account and Profile Questions #### My app uses anonymous user logins. Is AccountProfile still required for Engage? -`AccountProfile` is designed for apps that use individual user accounts. However, we understand that some apps, like yours, may rely on anonymous logins. Here's how Engage works in this scenario: +`AccountProfile` is designed for apps that use individual user accounts. +However, we understand that some apps, like yours, may rely on anonymous logins. +Here's how Engage works in this scenario: - `AccountProfile` is technically required, but you can still integrate Engage even if your app doesn't have a user account system. - Limited to on-device use: The cross-device capabilities of Engage relies on identifying users across different devices. Since anonymous logins don't provide this, the feature will be limited to the user's current device. - How to configure: To set this up, you'll need to disable cross-device syncing. This makes sure that continuation entries only appear on the specific device where the content was started. -In summary: While you can integrate Engage with anonymous logins, users will only be able to resume content on the same device. +In summary: While you can integrate Engage with anonymous logins, users will +only be able to resume content on the same device. #### Can I use AccountProfile with only `accountId` and no `profileId`, even when my app supports both accountId and `profileId`? -AccountProfile requires both `accountId` and `profileId` to function correctly. Here's why: +AccountProfile requires both `accountId` and `profileId` to function correctly. +Here's why: - Consistent identification: `accountId` identifies the user, while `profileId` distinguishes between different profiles within that user's account (if applicable). Providing both makes sure that Engage accurately tracks and displays content for each individual profile. - Preventing errors: Using `accountId` and `profileId` inconsistently across different API calls can lead to unexpected behavior and errors. For example, if you include both when adding content to Engage but only use `accountId` when deleting content, the system may not be able to correctly identify and remove the intended items. @@ -219,29 +293,49 @@ AccountProfile requires both `accountId` and `profileId` to function correctly. #### How does Google use the `profileId` on their side? -If the service offers different profiles to watch content, `accountId` and `profileId` would be used to associate the content watched on the device to the signed-in Google Account on the device. Google would record the ContinueWatching data against the combination of `accountId` and `profileId`. Any Google device that has that same Google Account logged in, would get the latest updated data from the same associated `accountId` and `profileId`, in its ContinueWatching row. +If the service offers different profiles to watch content, `accountId` and +`profileId` would be used to associate the content watched on the device to the +signed-in Google Account on the device. Google would record the ContinueWatching +data against the combination of `accountId` and `profileId`. Any Google device +that has that same Google Account logged in, would get the latest updated data +from the same associated `accountId` and `profileId`, in its ContinueWatching +row. #### Is account linking required to implement Engage? -Account Linking is not needed. It is being deprioritized and all related use cases will be covered by the new Device Entitlements API. +Account Linking is not needed. It is being deprioritized and all related use +cases will be covered by the new Device Entitlements API. ### Sync Across Devices Questions #### What does "sync across devices" mean when users give consent? -With the user's "sync across device" consent, the content they're watching will be saved to Google TV servers, letting them seamlessly pick up where they left off on any signed-in device. Without consent, their watch history remains local to the current device. +With the user's "sync across device" consent, the content they're watching will +be saved to Google TV servers, letting them seamlessly pick up where they left +off on any signed-in device. Without consent, their watch history remains local +to the current device. #### Can we set "sync across devices" to false? -The `setUserConsentToSyncAcrossDevices` flag controls whether a user's ContinuationCluster data is synchronized across their devices (TV, phone, tablet, etc.). If this flag is set to false, then continue watching content is only surfaced on same device. +The `setUserConsentToSyncAcrossDevices` flag controls whether a user's +ContinuationCluster data is synchronized across their devices (TV, phone, +tablet, etc.). If this flag is set to false, then continue watching content is +only surfaced on same device. -To get the most out of our cross-device feature, we strongly advise your app to obtain user consent and set SyncAcrossDevices to true. +To get the most out of our cross-device feature, we strongly advise your app to +obtain user consent and set SyncAcrossDevices to true. #### How is user consent for sharing watch history obtained on non-Android devices? What data points are shared to 3P servers from non-Android devices? -The consent is collected at the user level (profile or account level). Once consent is obtained, the continue watching payloads based on engagement can be sent anywhere so Google can reflect the users' ubiquity resumption state across all entities that they have partial or next engagement with, on any device (without having to re-ask consent on every device or platform). Partners will send the users latest continue watching state (as per spec) associated with profile ID (that was deposited on android). +The consent is collected at the user level (profile or account level). Once +consent is obtained, the continue watching payloads based on engagement can be +sent anywhere so Google can reflect the users' ubiquity resumption state across +all entities that they have partial or next engagement with, on any device +(without having to re-ask consent on every device or platform). Partners will +send the users latest continue watching state (as per spec) associated with +profile ID (that was deposited on android). ### REST API Questions @@ -253,19 +347,25 @@ The ETA for REST API is March 2025, this is documented in Engage Developer Docs. #### Is Engage replacing the Watch Next API? -Engage will be backward compatible on all Android TV devices that support the Watch Next API. To integrate on Google TV and other surfaces that support Engage, developers should use the Engage SDK. +Engage will be backward compatible on all Android TV devices that support the +Watch Next API. To integrate on Google TV and other surfaces that support +Engage, developers should use the Engage SDK. ### Testing and Integration Questions #### What is the difference between LastPlayBackPositionTimeMillis and duration? -LastPlayBackPositionTimeMillis should reflect the playback duration in milliseconds where the user stopped watching (e.g., 605000 ms for 10 minutes and 5 seconds). It should never be greater than the entity's total duration. +LastPlayBackPositionTimeMillis should reflect the playback duration in +milliseconds where the user stopped watching (e.g., 605000 ms for 10 minutes and +5 seconds). It should never be greater than the entity's total duration. -Whereas, **LastEngagementTime** is the timestamp when the user last engaged with the content. +Whereas, **LastEngagementTime** is the timestamp when the user last engaged with +the content. #### What are the test cases we should perform? -The following are test cases for Google TV that our QA performs. Similar test cases can be performed on other surfaces as well. +The following are test cases for Google TV that our QA performs. Similar test +cases can be performed on other surfaces as well. 1. Watch a video, which is longer than 20 minutes for about 5 minutes. Exit app. The video card should be displayed in the continue watching row. Note: We only display 5 cards per 3p app in CW 2. Selecting the newly appeared card in the continue watching row should continue playing the video from the right point in the video.Note: Any New or old content should resume playback from where it was left off last @@ -284,9 +384,15 @@ The following are test cases for Google TV that our QA performs. Similar test ca 15. episode 1 drops and doesn't reappear in continue watching row \[on second device and on turning on the test device\] 1. episode 2 (if available), should appear in continue watching row \[on second device and on turning on the test device\] 16. First scenario: TV1: GoogleAccount: mom, 3p account / profile: account 1 / profile_1. Watch content and verify CW data displays contents watched by 3P account_1/profile_1 -17. TV2: GoogleAccount: mom. Verify CW data from the first scenario. Now login to the 3p app as a different account. 3p account / profile: account_2 / profile_2. Watch content and verify CW data displays contents watched by 3p account_2/profile_2 +17. TV2: GoogleAccount: mom. Verify CW data from the first scenario. Now login + to the 3p app as a different account. 3p account / profile: account_2 / + profile_2. Watch content and verify CW data displays contents watched by 3p + account_2/profile_2 -18. GoogleAccount: mom. New device case /3P app not installed. On a new device(FDR the device), Verify CW displays data from the last used 3P app that was used by the GoogleAccount. Note: CW row shouldn't show 3P contents if the GAIA is not yet associated with a 3P profile on other device +18. GoogleAccount: mom. New device case /3P app not installed. On a new + device(FDR the device), Verify CW displays data from the last used 3P app + that was used by the GoogleAccount. Note: CW row shouldn't show 3P contents + if the GAIA is not yet associated with a 3P profile on other device 1. GoogleAccount: mom. New device case /3P app installed but not logged in. On a new device(FDR the device), Verify CW displays data from the last used 3P app that was used by the GoogleAccount. 19. @@ -298,7 +404,8 @@ The following are test cases for Google TV that our QA performs. Similar test ca #### We are not seeing continuation content showing up on Google TV iOS app. What happened? -You will need to send iOS deeplinks for content to appear in the continue watching row on iOS devices. +You will need to send iOS deeplinks for content to appear in the continue +watching row on iOS devices. #### How often should I update content information for the continue watching row? Should I update it frequently, like every 15 seconds? @@ -309,7 +416,8 @@ No, frequent updates are not recommended. Here's why: When to update content information for the continue watching row: -Focus on capturing meaningful changes in the user's viewing progress. Here are the key scenarios: +Focus on capturing meaningful changes in the user's viewing progress. Here are +the key scenarios: - Playback Paused or Stopped: When a user pauses or stops watching, send an update to store their current position. - App Closed or Backgrounded: If a user exits the app or switches to another app while watching a video, send an update to save their progress. @@ -317,12 +425,15 @@ Focus on capturing meaningful changes in the user's viewing progress. Here are t How to efficiently update: -Instead of timed updates, utilize events within your video player or app lifecycle to trigger updates. For example: +Instead of timed updates, utilize events within your video player or app +lifecycle to trigger updates. For example: - `onPause`, `onStop`: When the video playback pauses or stops. - `onAppClose`, `onAppBackgrounded`: When the app closes or moves to the background. -By following these guidelines, you'll ensure efficient use of resources while still providing a seamless experience in the continue watching row for your users. +By following these guidelines, you'll ensure efficient use of resources while +still providing a seamless experience in the continue watching row for your +users. > [!NOTE] > **Note:** Please contact [`engage-developers@google.com`](mailto:engage-developers@google.com) if you have any questions that are not covered here. \ No newline at end of file diff --git a/play/engage-sdk-integration/references/android/guide/playcore/engage/food.md b/play/engage-sdk-integration/references/android/guide/playcore/engage/food.md index 72aa976..d683a52 100644 --- a/play/engage-sdk-integration/references/android/guide/playcore/engage/food.md +++ b/play/engage-sdk-integration/references/android/guide/playcore/engage/food.md @@ -1,32 +1,59 @@ -Boost app engagement by reaching your users where they are. Integrate Engage SDK to deliver personalized recommendations and continuation content directly to users across multiple on-device surfaces, like **[Collections](https://android-developers.googleblog.com/2024/07/introducing-collections-powered-by-engage-sdk.html)** , **[Entertainment Space](https://blog.google/products/android/entertainment-space/)** , and the Play Store. The integration adds less than 50 KB (compressed) to the average APK and takes most apps about a week of developer time. Learn more at our **[business site](http://play.google.com/console/about/programs/EngageSDK)**. +Boost app engagement by reaching your users where they are. Integrate Engage SDK +to deliver personalized recommendations and continuation content directly to +users across multiple on-device surfaces, like +**[Collections](https://android-developers.googleblog.com/2024/07/introducing-collections-powered-by-engage-sdk.html)** , **[Entertainment +Space](https://blog.google/products/android/entertainment-space/)** , and the Play Store. The integration adds +less than 50 KB (compressed) to the average APK and takes most apps about a +week of developer time. Learn more at our **[business +site](http://play.google.com/console/about/programs/EngageSDK)**. -This guide contains instructions for developer partners to deliver food content (food ordering, food or restaurant reviews \& discovery, meal subscriptions, recipes) to Engage content surfaces. +This guide contains instructions for developer partners to deliver food content +(food ordering, food or restaurant reviews \& discovery, meal subscriptions, +recipes) to Engage content surfaces. ## Integration detail ### Terminology -This integration includes the following five cluster types: **Recommendation** , **Featured** , **Food Shopping Cart** , **Food Shopping List** , and **Reorder**. +This integration includes the following five cluster types: **Recommendation** , +**Featured** , **Food Shopping Cart** , **Food Shopping List** , and **Reorder**. -- **Recommendation** clusters show personalized food-related suggestions from an individual developer partner. These recommendations can be personalized to the user or generalized (for example, new on sale). Use them to surface recipes, stores, dishes, groceries, and so on as you see fit. +- **Recommendation** clusters show personalized food-related suggestions from an + individual developer partner. These recommendations can be personalized to the + user or generalized (for example, new on sale). Use them to surface recipes, + stores, dishes, groceries, and so on as you see fit. - A Recommendation cluster can be made of `ProductEntity`, `StoreEntity`, or `RecipeEntity` listings, but not a mix of different entity types. ![](https://developer.android.com/static/images/guide/playcore/engage/food-entities.png) **Figure :**\`ProductEntity\`, \`StoreEntity\`, and \`RecipeEntity\`. (\*UI for illustrative purposes only) -- The **Featured** cluster showcases a selection of entities from multiple developer partners in one UI grouping. There will be a single Featured cluster, which is surfaced near the top of the UI with a priority placement above all Recommendation clusters. Each developer partner will be allowed to broadcast up to 10 entities in the Featured cluster. +- The **Featured** cluster showcases a selection of entities from multiple + developer partners in one UI grouping. There will be a single Featured + cluster, which is surfaced near the top of the UI with a priority placement + above all Recommendation clusters. Each developer partner will be allowed to + broadcast up to 10 entities in the Featured cluster. ![](https://developer.android.com/static/images/guide/playcore/engage/food-featured.png) **Figure :** Featured cluster with the \`RecipeEntity\`. (\*UI for illustrative purposes only) -- The **Food Shopping Cart** cluster shows a sneak peek of grocery shopping carts from multiple developer partners in one UI grouping, prompting users to complete their outstanding carts. There is a single Food Shopping Cart cluster. +- The **Food Shopping Cart** cluster shows a sneak peek of grocery shopping + carts from multiple developer partners in one UI grouping, prompting users to + complete their outstanding carts. There is a single Food Shopping Cart + cluster. - - Food Shopping Cart Cluster must show the total count of items in the cart and may also include images for X items in the user's cart. + - Food Shopping Cart Cluster must show the total count of items in the + cart and may also include images for X items in the user's cart. ![](https://developer.android.com/static/images/guide/playcore/engage/food-shopping-cart.png) **Figure:** Food Shopping Cart cluster from a single partner. (\*UI for illustrative purposes only) -- The **Food Shopping List** cluster shows a sneak peek of the grocery shopping lists from multiple developer partners in one UI grouping, prompting users to return to the corresponding app to update and complete their lists. There is a single Food Shopping List cluster. +- The **Food Shopping List** cluster shows a sneak peek of the grocery shopping + lists from multiple developer partners in one UI grouping, prompting users to + return to the corresponding app to update and complete their lists. There is a + single Food Shopping List cluster. ![](https://developer.android.com/static/images/guide/playcore/engage/food-shopping-list.png) **Figure:** Food Shopping List cluster from a single partner. (\*UI for illustrative purposes only) -- The **Reorder** cluster shows a sneak peek of the previous orders from multiple developer partners in one UI grouping, prompting users to reorder. There is a single Reorder cluster. +- The **Reorder** cluster shows a sneak peek of the previous orders from + multiple developer partners in one UI grouping, prompting users to reorder. + There is a single Reorder cluster. - - Reorder cluster must show the total count of items in the user's previous order and must also include one of the following: + - Reorder cluster must show the total count of items in the + user's previous order and must also include one of the following: - Images for X items in the user's previous order. - Labels for X items in the user's previous order. @@ -46,9 +73,11 @@ Add the `com.google.android.engage:engage-core` library to your app: ### Summary -The design is based on an implementation of a [bound service](https://developer.android.com/guide/components/bound-services). +The design is based on an implementation of a [bound +service](https://developer.android.com/guide/components/bound-services). -The data a client can publish is subject to the following limits for different cluster types: +The data a client can publish is subject to the following limits for different +cluster types: | Cluster type | Cluster limits | Maximum entity limits in a cluster | |---|---|---| @@ -60,7 +89,8 @@ The data a client can publish is subject to the following limits for different c ### Step 1: Provide entity data -The SDK has defined different entities to represent each item type. We support the following entities for the Food category: +The SDK has defined different entities to represent each item type. We support +the following entities for the Food category: 1. `ProductEntity` 2. `StoreEntity` @@ -73,7 +103,9 @@ The charts below outline available attributes and requirements for each type. #### `ProductEntity` -The `ProductEntity` object represents an individual item (such as a grocery item, dish from a restaurant, or a promotion) that developer partners want to publish. +The `ProductEntity` object represents an individual item (such as a grocery +item, dish from a restaurant, or a promotion) that developer partners want to +publish.
@@ -101,7 +133,8 @@ The `ProductEntity` object represents an individual item (such as a grocery item #### `StoreEntity` -The `StoreEntity` object represents an individual store that developer partners want to publish, such as a restaurant or a grocery store. +The `StoreEntity` object represents an individual store that developer partners +want to publish, such as a restaurant or a grocery store.
@@ -127,7 +160,8 @@ The `StoreEntity` object represents an individual store that developer partners #### `RecipeEntity` -The `RecipeEntity` object represents a recipe item that developer partners want to publish. +The `RecipeEntity` object represents a recipe item that developer partners want +to publish.
@@ -230,7 +264,10 @@ PNG, JPG, static GIF, WebP ### Step 2: Provide Cluster data -It is recommended to have the content publish job executed in the background (for example, using [WorkManager](https://developer.android.com/topic/libraries/architecture/workmanager)) and scheduled on a regular basis or on an event basis (for example, every time the user opens the app or when the user just added something to their cart). +It is recommended to have the content publish job executed in the background +(for example, using [WorkManager](https://developer.android.com/topic/libraries/architecture/workmanager)) +and scheduled on a regular basis or on an event basis (for example, every time +the user opens the app or when the user just added something to their cart). `AppEngageFoodClient` is responsible for publishing food clusters. @@ -254,11 +291,11 @@ There are following APIs to publish clusters in the client: #### `isServiceAvailable` -This API is used to check if the service is available for integration and whether the content can be presented on the device. +This API is used to check if the service is available for integration and +whether the content can be presented on the device. ##### For Engage SDK v1.6.0 and higher (Recommended) -
## Android skills @@ -276,7 +313,11 @@ If your team uses AI coding tools (such as Gemini in Android Studio), you can au
-You can check the service availability for every cluster type that you intend to publish. The `isServiceAvailable` API accepts a request object, `ServiceAvailabilityRequest`, which contains the cluster types for which service availability needs to be checked. You can find the `ClusterType` enum values required for `ServiceAvailabilityRequest` from the following table. +You can check the service availability for every cluster type that you intend to +publish. The `isServiceAvailable` API accepts a request object, +`ServiceAvailabilityRequest`, which contains the cluster types for which service +availability needs to be checked. You can find the `ClusterType` enum values +required for `ServiceAvailabilityRequest` from the following table. | Cluster Type | Cluster Type Constant | Integer Value | |---|---|---| @@ -337,11 +378,26 @@ You can check the service availability for every cluster type that you intend to ###### Conditional Service Availability Feature -Some integrated apps request a special configuration that enables and disables the Engage service intermittently in order to reduce their serving cost. This intermittent content ingestion strategy, although possible, negatively affects the user and the product -- stale content will not be presented and some surfaces will not be served at all. +Some integrated apps request a special configuration that enables and disables +the Engage service intermittently in order to reduce their serving cost. This +intermittent content ingestion strategy, although possible, negatively affects +the user and the product -- stale content will not be presented and some surfaces +will not be served at all. -Starting with v1.6.0, the Engage SDK allows checking availability for specific cluster types. This provides more flexibility so that if the intermittent content strategy was adopted by a given application, some cluster types can follow that intermittent strategy while other cluster types are always enabled (i.e. continuation clusters). +Starting with v1.6.0, the Engage SDK allows checking availability for specific +cluster types. This provides more flexibility so that if the intermittent +content strategy was adopted by a given application, some cluster types can +follow that intermittent strategy while other cluster types are always enabled +(i.e. continuation clusters). -If the Engage service should not be 'continuously' enabled on all supported devices for whatever reason, and is configured for intermittent ingestion for any set of devices, all continuation cluster publications (e.g. Food Shopping Cart, Food Shopping List, and Reorder) will be still enabled by default configuration, and the rest of the cluster types will be enabled and disabled intermittently. If intermittent ingestion applies to you but this default configuration is not suitable for your needs, please contact engage-developers@google.com. +If the Engage service should not be 'continuously' enabled on all supported +devices for whatever reason, and is configured for intermittent ingestion for +any set of devices, all continuation cluster publications (e.g. Food Shopping +Cart, Food Shopping List, and Reorder) will be still enabled by default +configuration, and the rest of the cluster types will be enabled and disabled +intermittently. If intermittent ingestion applies to you but this default +configuration is not suitable for your needs, please contact +engage-developers@google.com. ##### For SDK versions prior to v1.6.0 (Deprecated) @@ -422,12 +478,14 @@ A `RecommendationCluster` object can have the following attributes: .build()) .build()); -When the service receives the request, the following actions take place within one transaction: +When the service receives the request, the following actions take place within +one transaction: - All existing Recommendation Cluster data is removed. - Data from the request is parsed and stored in new Recommendation Clusters. -In case of an error, the entire request is rejected and the existing state is maintained. +In case of an error, the entire request is rejected and the existing state is +maintained. #### `publishFeaturedCluster` @@ -456,12 +514,14 @@ This API is used to publish a `FeaturedCluster` object. .build()) .build()); -When the service receives the request, the following actions take place within one transaction: +When the service receives the request, the following actions take place within +one transaction: - Existing `FeaturedCluster` data from the developer partner is removed. - Data from the request is parsed and stored in the updated Featured Cluster. -In case of an error, the entire request is rejected and the existing state is maintained. +In case of an error, the entire request is rejected and the existing state is +maintained. #### `publishFoodShoppingCarts` @@ -490,12 +550,14 @@ This API is used to publish a list of `FoodShoppingCart` objects. .build()) .build()); -When the service receives the request, the following actions take place within one transaction: +When the service receives the request, the following actions take place within +one transaction: - Existing `FoodShoppingCart` data from the developer partner is removed. - Data from the request is parsed and stored in the updated Shopping Cart Cluster. -In case of an error, the entire request is rejected and the existing state is maintained. +In case of an error, the entire request is rejected and the existing state is +maintained. #### `publishFoodShoppingLists` @@ -524,12 +586,14 @@ This API is used to publish a list of `FoodShoppingList` objects. .build()) .build()); -When the service receives the request, the following actions take place within one transaction: +When the service receives the request, the following actions take place within +one transaction: - Existing `FoodShoppingList` data from the developer partner is removed. - Data from the request is parsed and stored in the updated Shopping List Cluster. -In case of an error, the entire request is rejected and the existing state is maintained. +In case of an error, the entire request is rejected and the existing state is +maintained. #### `publishReorderCluster` @@ -558,16 +622,20 @@ This API is used to publish a `FoodReorderCluster` object. .build()) .build()); -When the service receives the request, the following actions take place within one transaction: +When the service receives the request, the following actions take place within +one transaction: - Existing `FoodReorderCluster` data from the developer partner is removed. - Data from the request is parsed and stored in the updated Reorder Cluster. -In case of an error, the entire request is rejected and the existing state is maintained. +In case of an error, the entire request is rejected and the existing state is +maintained. #### `publishUserAccountManagementRequest` -This API is used to publish a Sign In card . The signin action directs users to the app's sign in page so that the app can publish content (or provide more personalized content) +This API is used to publish a Sign In card . The signin action directs users to +the app's sign in page so that the app can publish content (or provide more +personalized content) The following metadata is part of the Sign In Card - @@ -620,16 +688,21 @@ The following metadata is part of the Sign In Card - .setSignInCardEntity(SIGN_IN_CARD_ENTITY) .build()); -When the service receives the request, the following actions take place within one transaction: +When the service receives the request, the following actions take place within +one transaction: - Existing `UserAccountManagementCluster` data from the developer partner is removed. - Data from the request is parsed and stored in the updated UserAccountManagementCluster Cluster. -In case of an error, the entire request is rejected and the existing state is maintained. +In case of an error, the entire request is rejected and the existing state is +maintained. #### `updatePublishStatus` -If for any internal business reason, none of the clusters is published, we **strongly recommend** updating the publish status using the **updatePublishStatus** API. This is important because : +If for any internal business reason, none of the clusters is published, +we **strongly recommend** updating the publish status using the +**updatePublishStatus** API. +This is important because : - Providing the status in all scenarios, even when the content is published (STATUS == PUBLISHED), is critical to populate dashboards that use this explicit status to convey the health and other metrics of your integration. - If no content is published but the integration status isn't broken (STATUS == NOT_PUBLISHED), Google can avoid triggering alerts in the app health dashboards. It confirms that content is not published due to an **expected** situation from the provider's standpoint. @@ -669,7 +742,11 @@ The list of eligible publish status codes are : // Reach out to engage-developers@ before using this enum. AppEngagePublishStatusCode.NOT_PUBLISHED_OTHER -If the content is not published due to a user not logged in, Google would recommend publishing the Sign In Card. If for any reason providers are not able to publish the Sign In Card then we recommend calling the **updatePublishStatus** API with the status code **NOT_PUBLISHED_REQUIRES_SIGN_IN** +If the content is not published due to a user not logged in, +Google would recommend publishing the Sign In Card. +If for any reason providers are not able to publish the Sign In Card +then we recommend calling the **updatePublishStatus** API +with the status code **NOT_PUBLISHED_REQUIRES_SIGN_IN** ### Kotlin @@ -700,7 +777,9 @@ This API is used to delete the content of Recommendation Clusters. client.deleteRecommendationClusters(); -When the service receives the request, it removes the existing data from the Recommendation Clusters. In case of an error, the entire request is rejected and the existing state is maintained. +When the service receives the request, it removes the existing data from the +Recommendation Clusters. In case of an error, the entire request is rejected +and the existing state is maintained. > [!NOTE] > **Note:** This api is available from version 1.1.0 onwards. @@ -720,7 +799,9 @@ This API is used to delete the content of Featured Cluster. client.deleteFeaturedCluster(); -When the service receives the request, it removes the existing data from the Featured Cluster. In case of an error, the entire request is rejected and the existing state is maintained. +When the service receives the request, it removes the existing data from the +Featured Cluster. In case of an error, the entire request is rejected +and the existing state is maintained. > [!NOTE] > **Note:** This api is available from version 1.1.0 onwards. @@ -740,7 +821,9 @@ This API is used to delete the content of Food Shopping Cart Cluster. client.deleteFoodShoppingCartCluster(); -When the service receives the request, it removes the existing data from the Food Shopping Cart Cluster. In case of an error, the entire request is rejected and the existing state is maintained. +When the service receives the request, it removes the existing data from the +Food Shopping Cart Cluster. In case of an error, the entire request is rejected +and the existing state is maintained. > [!NOTE] > **Note:** This api is availaile from version 1.1.0 onwards. @@ -760,7 +843,9 @@ This API is used to delete the content of Food Shopping List Cluster. client.deleteFoodShoppingListCluster(); -When the service receives the request, it removes the existing data from the Food Shopping List Cluster. In case of an error, the entire request is rejected and the existing state is maintained. +When the service receives the request, it removes the existing data from the +Food Shopping List Cluster. In case of an error, the entire request is rejected +and the existing state is maintained. > [!NOTE] > **Note:** This api is available from version 1.1.0 onwards. @@ -780,7 +865,9 @@ This API is used to delete the content of FoodReorderCluster. client.deleteReorderCluster(); -When the service receives the request, it removes the existing data from the Reorder Cluster. In case of an error, the entire request is rejected and the existing state is maintained. +When the service receives the request, it removes the existing data from the +Reorder Cluster. In case of an error, the entire request is rejected +and the existing state is maintained. > [!NOTE] > **Note:** This api is available from version 1.1.0 onwards. @@ -800,7 +887,9 @@ This API is used to delete the content of UserAccountManagement Cluster. client.deleteUserManagementCluster(); -When the service receives the request, it removes the existing data from the UserAccountManagement Cluster. In case of an error, the entire request is rejected and the existing state is maintained. +When the service receives the request, it removes the existing data from the +UserAccountManagement Cluster. In case of an error, the entire request is +rejected and the existing state is maintained. > [!NOTE] > **Note:** This api is available from version 1.1.0 onwards. @@ -830,11 +919,15 @@ This API is used to delete the content of a given cluster type. ... .build()); -When the service receives the request, it removes the existing data from all clusters matching the specified cluster types. Clients can choose to pass one or many cluster types. In case of an error, the entire request is rejected and the existing state is maintained. +When the service receives the request, it removes the existing data from all +clusters matching the specified cluster types. Clients can choose to pass one or +many cluster types. In case of an error, the entire request is rejected and the +existing state is maintained. #### Error handling -It is highly recommended to listen to the task result from the publish APIs such that a follow-up action can be taken to recover and resubmit an successful task. +It is highly recommended to listen to the task result from the publish APIs such +that a follow-up action can be taken to recover and resubmit an successful task. client.publishRecommendationClusters( new PublishRecommendationClustersRequest.Builder() @@ -856,7 +949,8 @@ It is highly recommended to listen to the task result from the publish APIs such } }); -The error is returned as an `AppEngageException` with the cause included as an error code. +The error is returned as an `AppEngageException` with the cause included as an +error code. | Error code | Error name | Note | |---|---|---| @@ -870,13 +964,23 @@ The error is returned as an `AppEngageException` with the cause included as an e ### Step 3: Handle broadcast intents -In addition to making publish content API calls through a job, it is also required to set up a [`BroadcastReceiver`](https://developer.android.com/reference/android/content/BroadcastReceiver) to receive the request for a content publish. +In addition to making publish content API calls through a job, it is also +required to set up a +[`BroadcastReceiver`](https://developer.android.com/reference/android/content/BroadcastReceiver) to receive +the request for a content publish. -The goal of broadcast intents is mainly for app reactivation and forcing data sync. Broadcast intents are not designed to be sent very frequently. It is only triggered when the Engage Service determines the content might be stale (for example, a week old). That way, there is more confidence that the user can have a fresh content experience, even if the application has not been executed for a long period of time. +The goal of broadcast intents is mainly for app reactivation and forcing data +sync. Broadcast intents are not designed to be sent very frequently. It is only +triggered when the Engage Service determines the content might be stale (for +example, a week old). That way, there is more confidence that the user can have +a fresh content experience, even if the application has not been executed for a +long period of time. The `BroadcastReceiver` must be set up in the following two ways: -- Dynamically register an instance of the `BroadcastReceiver` class using `Context.registerReceiver()`. This enables communication from applications that are still live in memory. +- Dynamically register an instance of the `BroadcastReceiver` class using + `Context.registerReceiver()`. This enables communication from applications + that are still live in memory. ### Kotlin @@ -982,7 +1086,10 @@ The `BroadcastReceiver` must be set up in the following two ways: } -- Statically declare an implementation with the `` tag in your `AndroidManifest.xml` file. This allows the application to receive broadcast intents when it is not running, and also allows the application to publish the content. +- Statically declare an implementation with the `` tag in your + `AndroidManifest.xml` file. This allows the application to receive broadcast + intents when it is not running, and also allows the application to publish + the content. -The following [intents](https://developer.android.com/reference/android/content/Intent) will be sent by the service: +The following [intents](https://developer.android.com/reference/android/content/Intent) will be sent by the +service: - `com.google.android.engage.action.PUBLISH_RECOMMENDATION` It is recommended to start a `publishRecommendationClusters` call when receiving this intent. - `com.google.android.engage.action.PUBLISH_FEATURED` It is recommended to start a `publishFeaturedCluster` call when receiving this intent. @@ -1018,15 +1126,18 @@ The following [intents](https://developer.android.com/reference/android/content/ ## Integration workflow -For a step-by-step guide on verifying your integration after it is complete, see [Engage developer integration workflow](https://developer.android.com/guide/playcore/engage/workflow). +For a step-by-step guide on verifying your integration after it is complete, see +[Engage developer integration workflow](https://developer.android.com/guide/playcore/engage/workflow). ## FAQs -See [Engage SDK Frequently Asked Questions](https://developer.android.com/guide/playcore/engage/faq) for FAQs. +See [Engage SDK Frequently Asked Questions](https://developer.android.com/guide/playcore/engage/faq) for +FAQs. ## Contact -Contact [`engage-developers@google.com`](mailto:engage-developers@google.com) if there are any questions during the integration process. Our team will reply as soon as possible. +Contact [`engage-developers@google.com`](mailto:engage-developers@google.com) if there are any questions during +the integration process. Our team will reply as soon as possible. ## Next steps diff --git a/play/engage-sdk-integration/references/android/guide/playcore/engage/healthandfitness.md b/play/engage-sdk-integration/references/android/guide/playcore/engage/healthandfitness.md index 5cd2518..c9034e1 100644 --- a/play/engage-sdk-integration/references/android/guide/playcore/engage/healthandfitness.md +++ b/play/engage-sdk-integration/references/android/guide/playcore/engage/healthandfitness.md @@ -1,43 +1,83 @@ -Boost app engagement by reaching your users where they are. Integrate Engage SDK to deliver personalized recommendations and continuation content directly to users across multiple on-device surfaces, like **[Collections](https://android-developers.googleblog.com/2024/07/introducing-collections-powered-by-engage-sdk.html)** , **[Entertainment Space](https://blog.google/products/android/entertainment-space/)** , and the Play Store. The integration adds less than 50 KB (compressed) to the average APK and takes most apps about a week of developer time. Learn more at our **[business site](http://play.google.com/console/about/programs/EngageSDK)**. +Boost app engagement by reaching your users where they are. Integrate Engage SDK +to deliver personalized recommendations and continuation content directly to +users across multiple on-device surfaces, like +**[Collections](https://android-developers.googleblog.com/2024/07/introducing-collections-powered-by-engage-sdk.html)** , **[Entertainment +Space](https://blog.google/products/android/entertainment-space/)** , and the Play Store. The integration adds +less than 50 KB (compressed) to the average APK and takes most apps about a +week of developer time. Learn more at our **[business +site](http://play.google.com/console/about/programs/EngageSDK)**. -This guide contains instructions for developer partners to deliver health and fitness content to Engage content surfaces. +This guide contains instructions for developer partners to deliver health and +fitness content to Engage content surfaces. ## Integration detail ### Terminology -This integration includes the following three cluster types: **Recommendation** , **Featured** , and **Continuation**. +This integration includes the following three cluster types: **Recommendation** , +**Featured** , and **Continuation**. -- **Recommendation** clusters show personalized health and fitness suggestions from an individual developer partner. These recommendations can be personalized to the user or generalized (for example, trending fitness \& health). Use these to surface articles or people related to health and fitness. +- **Recommendation** clusters show personalized health and fitness suggestions + from an individual developer partner. These recommendations can be + personalized to the user or generalized (for example, trending fitness \& + health). Use these to surface articles or people related to health and + fitness. - A Recommendation cluster can be made of `ArticleEntity`, `PersonEntity`, or `EventEntity` but not a mix of different entity types. Your recommendations take the following structure: - - **Recommendation Cluster:** A UI view that contains a group of recommendations from the same developer partner. + - **Recommendation Cluster:** A UI view that contains a group of + recommendations from the same developer partner. - - **Entity:** An object representing a single item in a cluster. This integration offers some entities that would be surfaced using the Recommendation Cluster: + - **Entity:** An object representing a single item in a cluster. This + integration offers some entities that would be surfaced using the + Recommendation Cluster: - - **ArticleEntity**: ArticleEntity represents a recommendation for text-based content related to health \& fitness. It can be used for articles, blogposts, marketing content, news snippets, etc. + - **ArticleEntity**: ArticleEntity represents a recommendation for + text-based content related to health \& fitness. It can be used for + articles, blogposts, marketing content, news snippets, etc. ![](https://developer.android.com/static/images/guide/playcore/engage/article-entity-health-and-fitness.png) **Figure 1:** UI showing a single ArticleEntity within Recommendations cluster. - - **PersonEntity**: PersonEntity represents a person. The recommendations could be to highlight a coach or any person related to health and fitness, etc. + - **PersonEntity**: PersonEntity represents a person. The + recommendations could be to highlight a coach or any person related + to health and fitness, etc. ![](https://developer.android.com/static/images/guide/playcore/engage/person-entity-health-and-fitness.png) **Figure 2:** UI showing a single PersonEntity within Recommendations cluster. - - **EventEntity**: EventEntity represents an event happening in the future. Event start time is a critical piece of information that needs to be conveyed to the users This entity could be used for surfacing events like blood donation camp, training sessions, gym or yoga classes etc. related to health and fitness. + - **EventEntity**: EventEntity represents an event happening in the + future. Event start time is a critical piece of information that + needs to be conveyed to the users This entity could be used for + surfacing events like blood donation camp, training sessions, gym or + yoga classes etc. related to health and fitness. ![](https://developer.android.com/static/images/guide/playcore/engage/event-entity-health-and-fitness.png) **Figure 3:** UI showing a single EventEntity within Recommendations cluster. -- The **Continuation** cluster shows content recently engaged by users from multiple developer partners in a single UI grouping. Each developer partner will be allowed to broadcast a maximum of 10 entities in the Continuation cluster. +- The **Continuation** cluster shows content recently engaged by users from + multiple developer partners in a single UI grouping. Each developer partner + will be allowed to broadcast a maximum of 10 entities in the Continuation + cluster. Your continuation content can take the following structure: - - **ArticleEntity**: ArticleEntity represents a recommendation for text-based content that is related to health \& fitness. This entity can be used to represent unfinished news articles or other content that the user would like to continue consuming from where they left it. Ex: News snippet, blogpost snippet about health or fitness related topics. + - **ArticleEntity**: ArticleEntity represents a recommendation for + text-based content that is related to health \& fitness. This entity can + be used to represent unfinished news articles or other content that the + user would like to continue consuming from where they left it. Ex: News + snippet, blogpost snippet about health or fitness related topics. ![](https://developer.android.com/static/images/guide/playcore/engage/article-entity-continuation-health-and-fitness.png) **Figure 6.** UI showing a single ArticleEntity within a Continuation cluster. - - **EventReservationEntity**: EventReservationEntity represents reservation for an event and helps users track upcoming or ongoing fitness and health events reservations. Ex: Training sessions + - **EventReservationEntity**: EventReservationEntity represents + reservation for an event and helps users track upcoming or ongoing + fitness and health events reservations. Ex: Training sessions ![](https://developer.android.com/static/images/guide/playcore/engage/event-reservation-entity-health-and-fitness.png) **Figure 8.** UI showing a single EventReservationEntity within a Continuation cluster. -- The **Featured** cluster showcases a selection of entities from multiple developer partners in one UI grouping. There will be a single Featured cluster, which is surfaced near the top of the UI with a priority placement above all Recommendation clusters. Each developer partner will be allowed to broadcast up to 10 entities in the Featured cluster. +- The **Featured** cluster showcases a selection of entities from multiple + developer partners in one UI grouping. There will be a single Featured + cluster, which is surfaced near the top of the UI with a priority placement + above all Recommendation clusters. Each developer partner will be allowed to + broadcast up to 10 entities in the Featured cluster. - - **GenericFeaturedEntity**: GenericFeaturedEntity differs from Recommendation item in that Featured item should be used for a single top content from developers and should represent the single most important content that will be interesting and relevant to users. + - **GenericFeaturedEntity**: GenericFeaturedEntity differs from + Recommendation item in that Featured item should be used for a single + top content from developers and should represent the single most + important content that will be interesting and relevant to users. ![](https://developer.android.com/static/images/guide/playcore/engage/featured-item-health-and-fitness.png) **Figure 12:** UI showing a single hero GenericFeaturedEntity card within a Featured cluster @@ -54,9 +94,11 @@ Add the `com.google.android.engage:engage-core` library to your app: ### Summary -The design is based on an implementation of a [bound service](https://developer.android.com/guide/components/bound-services). +The design is based on an implementation of a +[bound service](https://developer.android.com/guide/components/bound-services). -The data a client can publish is subject to the following limits for different cluster types: +The data a client can publish is subject to the following limits for different +cluster types: | Cluster type | Cluster limits | Minimum entity limits in a cluster | Maximum entity limits in a cluster | |---|---|---|---| @@ -66,7 +108,8 @@ The data a client can publish is subject to the following limits for different c ### Step 1: Provide entity data -The SDK has defined different entities to represent each item type. We support the following entities for the Health \& Fitness category: +The SDK has defined different entities to represent each item type. We support +the following entities for the Health \& Fitness category: 1. `GenericFeaturedEntity` 2. `ArticleEntity` @@ -212,7 +255,8 @@ Required specifications for image assets are listed in this table: | Landscape (1.91x1) | 600x314 | 1200x628 | | Portrait (4x5) | 480x600 | 960x1200 | -The images are required to be hosted on public CDNs so that Google can access them. +The images are required to be hosted on public CDNs so that Google can access +them. *File formats* @@ -229,7 +273,8 @@ PNG, JPG, static GIF, WebP #### Content Category -The content category allows apps to publish content belonging to multiple categories. This maps the content with some of the predefined categories namely: +The content category allows apps to publish content belonging to multiple +categories. This maps the content with some of the predefined categories namely: - `TYPE_EDUCATION` - `TYPE_SPORTS` @@ -249,25 +294,33 @@ The content category allows apps to publish content belonging to multiple catego - `TYPE_PARENTING` - `TYPE_DATING` -The images are required to be hosted on public CDNs so that Google can access them. +The images are required to be hosted on public CDNs so that Google can access +them. *Guidelines to use the content categories* 1. Some entities like **ArticleEntity** and **GenericFeaturedEntity** are eligible to use any of the content categories. For other entities like **EventEntity** , **EventReservationEntity** , **PersonEntity**, only a subset of these categories are eligible. Check the list of categories eligible for an entity type before populating the list. -2. Use the specific entity type for some content categories over a combination of the Generic entities and the ContentCategory: +2. Use the specific entity type for some content categories over a combination + of the Generic entities and the ContentCategory: - TYPE_MOVIES_AND_TV_SHOWS - Check out the entities from [Watch integration guide](https://developer.android.com/guide/playcore/engage/watch) before using the generic entities. - TYPE_BOOKS - Check out the [EbookEntity](https://developer.android.com/guide/playcore/engage/read#ebookentity) before using the generic entities. - TYPE_AUDIOBOOKS - Check out [AudiobookEntity](https://developer.android.com/guide/playcore/engage/read#audiobookentity) before using the generic entities. - TYPE_SHOPPING - Check out [ShoppingEntity](https://developer.android.com/guide/playcore/engage/shopping#shoppingEntity) before using the generic entities. - TYPE_FOOD_AND_DRINK - Check out entities from [Food Integration guide](https://developer.android.com/guide/playcore/engage/food) before using the generic entities. -3. The ContentCategory field is optional and should be left blank if the content doesn't belong to any of the categories mentioned earlier. +3. The ContentCategory field is optional and should be left blank if the + content doesn't belong to any of the categories mentioned earlier. -4. In case multiple content categories are provided, provide them in the order of relevance to the content with the most relevant content category placed first in the list. +4. In case multiple content categories are provided, provide them in the order + of relevance to the content with the most relevant content category placed + first in the list. ### Step 2: Provide Cluster data -It is recommended to have the content publish job executed in the background (for example, using [WorkManager](https://developer.android.com/topic/libraries/architecture/workmanager)) and scheduled on a regular basis or on an event basis (for example, every time the user opens the app or when the user just added something to their cart). +It is recommended to have the content publish job executed in the background +(for example, using [WorkManager](https://developer.android.com/topic/libraries/architecture/workmanager)) +and scheduled on a regular basis or on an event basis (for example, every time +the user opens the app or when the user just added something to their cart). `AppEngagePublishClient` is responsible for publishing clusters. @@ -287,11 +340,11 @@ There are following APIs to publish clusters in the client: #### `isServiceAvailable` -This API is used to check if the service is available for integration and whether the content can be presented on the device. +This API is used to check if the service is available for integration and +whether the content can be presented on the device. ##### For Engage SDK v1.6.0 and higher (Recommended) -
## Android skills @@ -309,7 +362,11 @@ If your team uses AI coding tools (such as Gemini in Android Studio), you can au
-You can check the service availability for every cluster type that you intend to publish. The `isServiceAvailable` API accepts a request object, `ServiceAvailabilityRequest`, which contains the cluster types for which service availability needs to be checked. You can find the `ClusterType` enum values required for `ServiceAvailabilityRequest` from the following table. +You can check the service availability for every cluster type that you intend to +publish. The `isServiceAvailable` API accepts a request object, +`ServiceAvailabilityRequest`, which contains the cluster types for which service +availability needs to be checked. You can find the `ClusterType` enum values +required for `ServiceAvailabilityRequest` from the following table. | Cluster Type | Cluster Type Constant | Integer Value | |---|---|---| @@ -367,11 +424,26 @@ You can check the service availability for every cluster type that you intend to ###### Conditional Service Availability Feature -Some integrated apps request a special configuration that enables and disables the Engage service intermittently in order to reduce their serving cost. This intermittent content ingestion strategy, although possible, negatively affects the user and the product -- stale content will not be presented and some surfaces will not be served at all. +Some integrated apps request a special configuration that enables and disables +the Engage service intermittently in order to reduce their serving cost. This +intermittent content ingestion strategy, although possible, negatively affects +the user and the product -- stale content will not be presented and some surfaces +will not be served at all. -Starting with v1.6.0, the Engage SDK allows checking availability for specific cluster types. This provides more flexibility so that if the intermittent content strategy was adopted by a given application, some cluster types can follow that intermittent strategy while other cluster types are always enabled (i.e. continuation clusters). +Starting with v1.6.0, the Engage SDK allows checking availability for specific +cluster types. This provides more flexibility so that if the intermittent +content strategy was adopted by a given application, some cluster types can +follow that intermittent strategy while other cluster types are always enabled +(i.e. continuation clusters). -If the Engage service should not be 'continuously' enabled on all supported devices for whatever reason, and is configured for intermittent ingestion for any set of devices, all continuation cluster publications (e.g. Continue Reading and Event Reservations) will be still enabled by default configuration, and the rest of the cluster types will be enabled and disabled intermittently. If intermittent ingestion applies to you but this default configuration is not suitable for your needs, please contact engage-developers@google.com. +If the Engage service should not be 'continuously' enabled on all supported +devices for whatever reason, and is configured for intermittent ingestion for +any set of devices, all continuation cluster publications (e.g. Continue +Reading and Event Reservations) will be still enabled by default +configuration, and the rest of the cluster types will be enabled and disabled +intermittently. If intermittent ingestion applies to you but this default +configuration is not suitable for your needs, please contact +engage-developers@google.com. ##### For SDK versions prior to v1.6.0 (Deprecated) @@ -445,12 +517,14 @@ This API is used to publish a list of `RecommendationCluster` objects. .build()) .build()); -When the service receives the request, the following actions take place within one transaction: +When the service receives the request, the following actions take place within +one transaction: - Existing `RecommendationCluster` data from the developer partner is removed. - Data from the request is parsed and stored in the updated Recommendation Cluster. -In case of an error, the entire request is rejected and the existing state is maintained. +In case of an error, the entire request is rejected and the existing state is +maintained. #### `publishFeaturedCluster` @@ -481,12 +555,14 @@ This API is used to publish a list of `FeaturedCluster` objects. .build()) .build()); -When the service receives the request, the following actions take place within one transaction: +When the service receives the request, the following actions take place within +one transaction: - Existing `FeaturedCluster` data from the developer partner is removed. - Data from the request is parsed and stored in the updated Featured Cluster. -In case of an error, the entire request is rejected and the existing state is maintained. +In case of an error, the entire request is rejected and the existing state is +maintained. #### `publishContinuationCluster` @@ -517,16 +593,20 @@ This API is used to publish a `ContinuationCluster` object. .build()) .build()); -When the service receives the request, the following actions take place within one transaction: +When the service receives the request, the following actions take place within +one transaction: - Existing `ContinuationCluster` data from the developer partner is removed. - Data from the request is parsed and stored in the updated Continuation Cluster. -In case of an error, the entire request is rejected and the existing state is maintained. +In case of an error, the entire request is rejected and the existing state is +maintained. #### `publishUserAccountManagementRequest` -This API is used to publish a Sign In card . The signin action directs users to the app's sign in page so that the app can publish content (or provide more personalized content) +This API is used to publish a Sign In card . The signin action directs users to +the app's sign in page so that the app can publish content (or provide more +personalized content) The following metadata is part of the Sign In Card - @@ -579,16 +659,20 @@ The following metadata is part of the Sign In Card - .setSignInCardEntity(SIGN_IN_CARD_ENTITY) .build()); -When the service receives the request, the following actions take place within one transaction: +When the service receives the request, the following actions take place within +one transaction: - Existing `UserAccountManagementCluster` data from the developer partner is removed. - Data from the request is parsed and stored in the updated UserAccountManagementCluster Cluster. -In case of an error, the entire request is rejected and the existing state is maintained. +In case of an error, the entire request is rejected and the existing state is +maintained. #### `updatePublishStatus` -If for any internal business reason, none of the clusters is published, we **strongly recommend** updating the publish status using the **updatePublishStatus** API. This is important because : +If for any internal business reason, none of the clusters is published, we +**strongly recommend** updating the publish status using the +**updatePublishStatus** API. This is important because : - Providing the status in all scenarios, even when the content is published (STATUS == PUBLISHED), is critical to populate dashboards that use this explicit status to convey the health and other metrics of your integration. - If no content is published but the integration status isn't broken (STATUS == NOT_PUBLISHED), Google can avoid triggering alerts in the app health dashboards. It confirms that content is not published due to an **expected** situation from the provider's standpoint. @@ -628,7 +712,11 @@ The list of eligible publish status codes are : // Reach out to engage-developers@ before using this enum. AppEngagePublishStatusCode.NOT_PUBLISHED_OTHER -If the content is not published due to a user not logged in, Google would recommend publishing the Sign In Card. If for any reason providers are not able to publish the Sign In Card then we recommend calling the **updatePublishStatus** API with the status code **NOT_PUBLISHED_REQUIRES_SIGN_IN** +If the content is not published due to a user not logged in, Google would +recommend publishing the Sign In Card. If for any reason providers are not able +to publish the Sign In Card then we recommend calling the +**updatePublishStatus** API with the status code +**NOT_PUBLISHED_REQUIRES_SIGN_IN** ### Kotlin @@ -659,7 +747,9 @@ This API is used to delete the content of Recommendation Clusters. client.deleteRecommendationClusters(); -When the service receives the request, it removes the existing data from the Recommendation Clusters. In case of an error, the entire request is rejected and the existing state is maintained. +When the service receives the request, it removes the existing data from the +Recommendation Clusters. In case of an error, the entire request is rejected and +the existing state is maintained. > [!NOTE] > **Note:** This api is available from version 1.1.0 onwards. @@ -679,7 +769,9 @@ This API is used to delete the content of Featured Cluster. client.deleteFeaturedCluster(); -When the service receives the request, it removes the existing data from the Featured Cluster. In case of an error, the entire request is rejected and the existing state is maintained. +When the service receives the request, it removes the existing data from the +Featured Cluster. In case of an error, the entire request is rejected and the +existing state is maintained. > [!NOTE] > **Note:** This api is available from version 1.1.0 onwards. @@ -699,7 +791,9 @@ This API is used to delete the content of Continuation Cluster. client.deleteContinuationCluster(); -When the service receives the request, it removes the existing data from the Continuation Cluster. In case of an error, the entire request is rejected and the existing state is maintained. +When the service receives the request, it removes the existing data from the +Continuation Cluster. In case of an error, the entire request is rejected and +the existing state is maintained. > [!NOTE] > **Note:** This api is available from version 1.1.0 onwards. @@ -719,7 +813,9 @@ This API is used to delete the content of UserAccountManagement Cluster. client.deleteUserManagementCluster(); -When the service receives the request, it removes the existing data from the UserAccountManagement Cluster. In case of an error, the entire request is rejected and the existing state is maintained. +When the service receives the request, it removes the existing data from the +UserAccountManagement Cluster. In case of an error, the entire request is +rejected and the existing state is maintained. > [!NOTE] > **Note:** This api is available from version 1.1.0 onwards. @@ -749,11 +845,15 @@ This API is used to delete the content of a given cluster type. .addClusterType(ClusterType.TYPE_RECOMMENDATION) .build()); -When the service receives the request, it removes the existing data from all clusters matching the specified cluster types. Clients can choose to pass one or many cluster types. In case of an error, the entire request is rejected and the existing state is maintained. +When the service receives the request, it removes the existing data from all +clusters matching the specified cluster types. Clients can choose to pass one or +many cluster types. In case of an error, the entire request is rejected and the +existing state is maintained. #### Error handling -It is highly recommended to listen to the task result from the publish APIs such that a follow-up action can be taken to recover and resubmit an successful task. +It is highly recommended to listen to the task result from the publish APIs such +that a follow-up action can be taken to recover and resubmit an successful task. ### Kotlin @@ -797,7 +897,8 @@ It is highly recommended to listen to the task result from the publish APIs such } }); -The error is returned as an `AppEngageException` with the cause included as an error code. +The error is returned as an `AppEngageException` with the cause included as an +error code. | Error code | Error name | Note | |---|---|---| @@ -811,13 +912,23 @@ The error is returned as an `AppEngageException` with the cause included as an e ### Step 3: Handle broadcast intents -In addition to making publish content API calls through a job, it is also required to set up a [`BroadcastReceiver`](https://developer.android.com/reference/android/content/BroadcastReceiver) to receive the request for a content publish. +In addition to making publish content API calls through a job, it is also +required to set up a +[`BroadcastReceiver`](https://developer.android.com/reference/android/content/BroadcastReceiver) to receive +the request for a content publish. -The goal of broadcast intents is mainly for app reactivation and forcing data sync. Broadcast intents are not designed to be sent very frequently. It is only triggered when the Engage Service determines the content might be stale (for example, a week old). That way, there is more confidence that the user can have a fresh content experience, even if the application has not been executed for a long period of time. +The goal of broadcast intents is mainly for app reactivation and forcing data +sync. Broadcast intents are not designed to be sent very frequently. It is only +triggered when the Engage Service determines the content might be stale (for +example, a week old). That way, there is more confidence that the user can have +a fresh content experience, even if the application has not been executed for a +long period of time. The `BroadcastReceiver` must be set up in the following two ways: -- Dynamically register an instance of the `BroadcastReceiver` class using `Context.registerReceiver()`. This enables communication from applications that are still live in memory. +- Dynamically register an instance of the `BroadcastReceiver` class using + `Context.registerReceiver()`. This enables communication from applications + that are still live in memory. ### Kotlin @@ -888,7 +999,10 @@ The `BroadcastReceiver` must be set up in the following two ways: } -- Statically declare an implementation with the `` tag in your `AndroidManifest.xml` file. This allows the application to receive broadcast intents when it is not running, and also allows the application to publish the content. +- Statically declare an implementation with the `` tag in your + `AndroidManifest.xml` file. This allows the application to receive broadcast + intents when it is not running, and also allows the application to publish + the content. -The following [intents](https://developer.android.com/reference/android/content/Intent) is sent by the service: +The following [intents](https://developer.android.com/reference/android/content/Intent) is sent by the +service: - `com.google.android.engage.action.PUBLISH_RECOMMENDATION` It is recommended to start a `publishRecommendationClusters` call when receiving this intent. - `com.google.android.engage.action.PUBLISH_FEATURED` It is recommended to start a `publishFeaturedCluster` call when receiving this intent. @@ -916,15 +1031,19 @@ The following [intents](https://developer.android.com/reference/android/content/ ## Integration workflow -For a step-by-step guide on verifying your integration after it is complete, see [Engage developer integration workflow](https://developer.android.com/guide/playcore/engage/workflow). +For a step-by-step guide on verifying your integration after it is complete, see +[Engage developer integration workflow](https://developer.android.com/guide/playcore/engage/workflow). ## FAQs -See [Engage SDK Frequently Asked Questions](https://developer.android.com/guide/playcore/engage/faq) for FAQs. +See [Engage SDK Frequently Asked Questions](https://developer.android.com/guide/playcore/engage/faq) for +FAQs. ## Contact -Contact [`engage-developers@google.com`](mailto:engage-developers@google.com) if there are any questions during the integration process. +Contact +[`engage-developers@google.com`](mailto:engage-developers@google.com) if there are +any questions during the integration process. ## Next steps diff --git a/play/engage-sdk-integration/references/android/guide/playcore/engage/listen.md b/play/engage-sdk-integration/references/android/guide/playcore/engage/listen.md index 1285698..8acb9c0 100644 --- a/play/engage-sdk-integration/references/android/guide/playcore/engage/listen.md +++ b/play/engage-sdk-integration/references/android/guide/playcore/engage/listen.md @@ -1,26 +1,47 @@ -Boost app engagement by reaching your users where they are. Integrate Engage SDK to deliver personalized recommendations and continuation content directly to users across multiple on-device surfaces, like **[Collections](https://android-developers.googleblog.com/2024/07/introducing-collections-powered-by-engage-sdk.html)** , **[Entertainment Space](https://blog.google/products/android/entertainment-space/)** , and the Play Store. The integration adds less than 50 KB (compressed) to the average APK and takes most apps about a week of developer time. Learn more at our **[business site](http://play.google.com/console/about/programs/EngageSDK)**. +Boost app engagement by reaching your users where they are. Integrate Engage SDK +to deliver personalized recommendations and continuation content directly to +users across multiple on-device surfaces, like +**[Collections](https://android-developers.googleblog.com/2024/07/introducing-collections-powered-by-engage-sdk.html)** , **[Entertainment +Space](https://blog.google/products/android/entertainment-space/)** , and the Play Store. The integration adds +less than 50 KB (compressed) to the average APK and takes most apps about a +week of developer time. Learn more at our **[business +site](http://play.google.com/console/about/programs/EngageSDK)**. -This guide contains instructions for developer partners to deliver audio content (music, podcasts, audiobooks, live radio) to Engage content surfaces. +This guide contains instructions for developer partners to deliver audio content +(music, podcasts, audiobooks, live radio) to Engage content surfaces. ## Integration detail ### Terminology -This integration includes the following three cluster types: **Recommendation** , **Continuation** , and **Featured**. +This integration includes the following three cluster types: **Recommendation** , +**Continuation** , and **Featured**. -- **Recommendation** clusters show personalized suggestions for content to read from an individual developer partner. +- **Recommendation** clusters show personalized suggestions for content to read + from an individual developer partner. Your recommendations take the following structure: - - **Recommendation Cluster:** A UI view that contains a group of recommendations from the same developer partner. + - **Recommendation Cluster:** A UI view that contains a group of + recommendations from the same developer partner. ![](https://developer.android.com/static/images/guide/playcore/engage/listen-term-1.png) **Figure 1.** Entertainment Space UI showing a Recommendation Cluster from a single partner. - - **Entity:** An object representing a single item in a cluster. An entity can be a playlist, an audiobook, a podcast, and more. See the [Provide entity data](https://developer.android.com/guide/playcore/engage/listen#provide-entity-data) section for a list of supported entity types. + - **Entity:** An object representing a single item in a cluster. An entity + can be a playlist, an audiobook, a podcast, and more. See the [Provide + entity data](https://developer.android.com/guide/playcore/engage/listen#provide-entity-data) section for a list of supported entity + types. ![](https://developer.android.com/static/images/guide/playcore/engage/listen-term-2.png) **Figure 2.** Entertainment Space UI showing a single Entity within a single partner's Recommendation Cluster. -- The **Continuation** cluster shows audio content recently engaged by users from multiple developer partners in a single UI grouping. Each developer partner will be allowed to broadcast a maximum of 10 entities in the Continuation cluster. +- The **Continuation** cluster shows audio content recently engaged by users + from multiple developer partners in a single UI grouping. Each developer + partner will be allowed to broadcast a maximum of 10 entities in the + Continuation cluster. ![](https://developer.android.com/static/images/guide/playcore/engage/listen-term-3.png) **Figure 3.** Entertainment Space UI showing a Continuation cluster with unfinished recommendations from multiple partners (only one recommendation is currently visible). -- The **Featured** cluster showcases a selection of items from multiple developer partners in a single UI grouping. There will be a single Featured cluster, which will be surfaced near the top of the UI with a priority placement above all Recommendation clusters. Each developer partner will be allowed to broadcast up to 10 entities in the Featured cluster. +- The **Featured** cluster showcases a selection of items from multiple + developer partners in a single UI grouping. There will be a single Featured + cluster, which will be surfaced near the top of the UI with a priority + placement above all Recommendation clusters. Each developer partner will be + allowed to broadcast up to 10 entities in the Featured cluster. ![](https://developer.android.com/static/images/guide/playcore/engage/listen-term-4.png) **Figure 4.** Entertainment Space UI showing a Featured cluster with recommendations from multiple partners (only one recommendation is currently visible). @@ -37,9 +58,11 @@ Add the `com.google.android.engage:engage-core` library to your app: ### Summary -The design is based on an implementation of a [bound service](https://developer.android.com/guide/components/bound-services). +The design is based on an implementation of a [bound +service](https://developer.android.com/guide/components/bound-services). -The data a client can publish is subject to the following limits for different cluster types: +The data a client can publish is subject to the following limits for different +cluster types: | Cluster type | Cluster limits | Maximum entity limits in a cluster | |---|---|---| @@ -49,7 +72,8 @@ The data a client can publish is subject to the following limits for different c ### Step 1: Provide entity data -The SDK has defined different entities to represent each item type. We support the following entities for the Listen category: +The SDK has defined different entities to represent each item type. We support +the following entities for the Listen category: 1. `MusicAlbumEntity` 2. `MusicArtistEntity` @@ -65,7 +89,8 @@ The charts below outline available attributes and requirements for each type. #### `MusicAlbumEntity` -The `MusicAlbumEntity` object represents a music album (for example, *Midnights* by Taylor Swift). +The `MusicAlbumEntity` object represents a music album (for example, *Midnights* +by Taylor Swift). | Attribute | Requirement | Notes | |---|---|---| @@ -101,7 +126,8 @@ The `MusicArtistEntity` object represents a music arist (for example, Adele). #### `MusicTrackEntity` -The `MusicTrackEntity` object represents a music track (for example, *Yellow* by Coldplay). +The `MusicTrackEntity` object represents a music track (for example, *Yellow* by +Coldplay). | Attribute | Requirement | Notes | |---|---|---| @@ -120,7 +146,8 @@ The `MusicTrackEntity` object represents a music track (for example, *Yellow* by #### `MusicVideoEntity` -The `MusicVideoEntity` object represents a music video (for example, *The Weeknd - Take My Breath (Official Music Video)*). +The `MusicVideoEntity` object represents a music video (for example, +*The Weeknd - Take My Breath (Official Music Video)*). | Attribute | Requirement | Notes | |---|---|---| @@ -140,7 +167,8 @@ The `MusicVideoEntity` object represents a music video (for example, *The Weeknd #### `PlaylistEntity` -The `PlaylistEntity` object represents a music playlist (for example, the US Top 10 Playlist). +The `PlaylistEntity` object represents a music playlist (for example, the US Top +10 Playlist). | Attribute | Requirement | Notes | |---|---|---| @@ -158,7 +186,8 @@ The `PlaylistEntity` object represents a music playlist (for example, the US Top #### `PodcastSeriesEntity` -The `PodcastSeriesEntity` object represents a podcast series (for example, *This American Life*). +The `PodcastSeriesEntity` object represents a podcast series (for example, *This +American Life*). | Attribute | Requirement | Notes | |---|---|---| @@ -177,7 +206,8 @@ The `PodcastSeriesEntity` object represents a podcast series (for example, *This #### `PodcastEpisodeEntity` -The `PodcastEpisodeEntity` object represents a podcast series (for example, *Spark Bird, Episode 754: This American Life*). +The `PodcastEpisodeEntity` object represents a podcast series (for example, +*Spark Bird, Episode 754: This American Life*). | Attribute | Requirement | Notes | |---|---|---| @@ -202,7 +232,8 @@ The `PodcastEpisodeEntity` object represents a podcast series (for example, *Spa #### `LiveRadioStationEntity` -The `LiveRadioStationEntity` object represents a live radio station (for example, 98.1 The Breeze). +The `LiveRadioStationEntity` object represents a live radio station (for +example, 98.1 The Breeze). | Attribute | Requirement | Notes | |---|---|---| @@ -218,7 +249,8 @@ The `LiveRadioStationEntity` object represents a live radio station (for example #### `AudiobookEntity` -The `AudiobookEntity` object represents an audiobook (for example, the audiobook of *Becoming* by Michelle Obama). +The `AudiobookEntity` object represents an audiobook (for example, the audiobook +of *Becoming* by Michelle Obama). | Attribute | Requirement | Notes | |---|---|---| @@ -306,9 +338,13 @@ PNG, JPG, static GIF, WebP ### Step 2: Provide Cluster data -It's recommended to have the content publish job executed in the background (for example, using [WorkManager](https://developer.android.com/topic/libraries/architecture/workmanager)) and scheduled on a regular basis or on an event basis (for example, every time the user opens the app or when the user just added something to their cart). +It's recommended to have the content publish job executed in the background +(for example, using [WorkManager](https://developer.android.com/topic/libraries/architecture/workmanager)) +and scheduled on a regular basis or on an event basis (for example, every time +the user opens the app or when the user just added something to their cart). -`AppEngagePublishClient` is responsible for publishing clusters. Following APIs are available in the client: +`AppEngagePublishClient` is responsible for publishing clusters. Following +APIs are available in the client: - `isServiceAvailable` - `publishRecommendationClusters` @@ -324,11 +360,11 @@ It's recommended to have the content publish job executed in the background (for #### `isServiceAvailable` -This API is used to check if the service is available for integration and whether the content can be presented on the device. +This API is used to check if the service is available for integration and +whether the content can be presented on the device. ##### For Engage SDK v1.6.0 and higher (Recommended) -
## Android skills @@ -346,7 +382,11 @@ If your team uses AI coding tools (such as Gemini in Android Studio), you can au
-You can check the service availability for every cluster type that you intend to publish. The `isServiceAvailable` API accepts a request object, `ServiceAvailabilityRequest`, which contains the cluster types for which service availability needs to be checked. You can find the `ClusterType` enum values required for `ServiceAvailabilityRequest` from the following table. +You can check the service availability for every cluster type that you intend to +publish. The `isServiceAvailable` API accepts a request object, +`ServiceAvailabilityRequest`, which contains the cluster types for which service +availability needs to be checked. You can find the `ClusterType` enum values +required for `ServiceAvailabilityRequest` from the following table. | Cluster Type | Cluster Type Constant | Integer Value | |---|---|---| @@ -404,11 +444,25 @@ You can check the service availability for every cluster type that you intend to ###### Conditional Service Availability Feature -Some integrated apps request a special configuration that enables and disables the Engage service intermittently in order to reduce their serving cost. This intermittent content ingestion strategy, although possible, negatively affects the user and the product -- stale content will not be presented and some surfaces will not be served at all. +Some integrated apps request a special configuration that enables and disables +the Engage service intermittently in order to reduce their serving cost. This +intermittent content ingestion strategy, although possible, negatively affects +the user and the product -- stale content will not be presented and some surfaces +will not be served at all. -Starting with v1.6.0, the Engage SDK allows checking availability for specific cluster types. This provides more flexibility so that if the intermittent content strategy was adopted by a given application, some cluster types can follow that intermittent strategy while other cluster types are always enabled (i.e. continuation clusters). +Starting with v1.6.0, the Engage SDK allows checking availability for specific +cluster types. This provides more flexibility so that if the intermittent +content strategy was adopted by a given application, some cluster types can +follow that intermittent strategy while other cluster types are always enabled +(i.e. continuation clusters). -If the Engage service should not be 'continuously' enabled on all supported devices for whatever reason, and is configured for intermittent ingestion for any set of devices, all continuation cluster publications (e.g. Continue Listening) will be still enabled by default configuration, and the rest of the cluster types will be enabled and disabled intermittently. If intermittent ingestion applies to you but this default configuration is not suitable for your needs, please contact engage-developers@google.com. +If the Engage service should not be 'continuously' enabled on all supported +devices for whatever reason, and is configured for intermittent ingestion for +any set of devices, all continuation cluster publications (e.g. Continue +Listening) will be still enabled by default configuration, and the rest of +the cluster types will be enabled and disabled intermittently. If +intermittent ingestion applies to you but this default configuration is not +suitable for your needs, please contact engage-developers@google.com. ##### For SDK versions prior to v1.6.0 (Deprecated) @@ -480,12 +534,14 @@ This API is used to publish a list of `RecommendationCluster` objects. .build()) .build()); -When the service receives the request, the following actions take place within one transaction: +When the service receives the request, the following actions take place within +one transaction: - Existing `RecommendationCluster` data from the developer partner is removed. - Data from the request is parsed and stored in the updated Recommendation Cluster. -In case of an error, the entire request is rejected and the existing state is maintained. +In case of an error, the entire request is rejected and the existing state is +maintained. #### `publishFeaturedCluster` @@ -514,12 +570,14 @@ This API is used to publish a list of `FeaturedCluster` objects. .build()) .build()); -When the service receives the request, the following actions take place within one transaction: +When the service receives the request, the following actions take place within +one transaction: - Existing `FeaturedCluster` data from the developer partner is removed. - Data from the request is parsed and stored in the updated Featured Cluster. -In case of an error, the entire request is rejected and the existing state is maintained. +In case of an error, the entire request is rejected and the existing state is +maintained. #### `publishContinuationCluster` @@ -550,16 +608,20 @@ This API is used to publish a `ContinuationCluster` object. .build()) .build()) -When the service receives the request, the following actions take place within one transaction: +When the service receives the request, the following actions take place within +one transaction: - Existing `ContinuationCluster` data from the developer partner is removed. - Data from the request is parsed and stored in the updated Continuation Cluster. -In case of an error, the entire request is rejected and the existing state is maintained. +In case of an error, the entire request is rejected and the existing state is +maintained. #### `publishUserAccountManagementRequest` -This API is used to publish a Sign In card . The signin action directs users to the app's sign in page so that the app can publish content (or provide more personalized content) +This API is used to publish a Sign In card . The signin action directs users to +the app's sign in page so that the app can publish content (or provide more +personalized content) The following metadata is part of the Sign In Card - @@ -612,16 +674,21 @@ The following metadata is part of the Sign In Card - .setSignInCardEntity(SIGN_IN_CARD_ENTITY) .build()); -When the service receives the request, the following actions take place within one transaction: +When the service receives the request, the following actions take place within +one transaction: - Existing `UserAccountManagementCluster` data from the developer partner is removed. - Data from the request is parsed and stored in the updated UserAccountManagementCluster Cluster. -In case of an error, the entire request is rejected and the existing state is maintained. +In case of an error, the entire request is rejected and the existing state is +maintained. #### `updatePublishStatus` -If for any internal business reason, none of the clusters is published, we **strongly recommend** updating the publish status using the **updatePublishStatus** API. This is important because : +If for any internal business reason, none of the clusters is published, +we **strongly recommend** updating the publish status using the +**updatePublishStatus** API. +This is important because : - Providing the status in all scenarios, even when the content is published (STATUS == PUBLISHED), is critical to populate dashboards that use this explicit status to convey the health and other metrics of your integration. - If no content is published but the integration status isn't broken (STATUS == NOT_PUBLISHED), Google can avoid triggering alerts in the app health dashboards. It confirms that content is not published due to an **expected** situation from the provider's standpoint. @@ -661,7 +728,11 @@ The list of eligible publish status codes are : // Reach out to engage-developers@ before using this enum. AppEngagePublishStatusCode.NOT_PUBLISHED_OTHER -If the content is not published due to a user not logged in, Google would recommend publishing the Sign In Card. If for any reason providers are not able to publish the Sign In Card then we recommend calling the **updatePublishStatus** API with the status code **NOT_PUBLISHED_REQUIRES_SIGN_IN** +If the content is not published due to a user not logged in, +Google would recommend publishing the Sign In Card. +If for any reason providers are not able to publish the Sign In Card +then we recommend calling the **updatePublishStatus** API +with the status code **NOT_PUBLISHED_REQUIRES_SIGN_IN** ### Kotlin @@ -692,7 +763,9 @@ This API is used to delete the content of Recommendation Clusters. client.deleteRecommendationClusters(); -When the service receives the request, it removes the existing data from the Recommendation Clusters. In case of an error, the entire request is rejected and the existing state is maintained. +When the service receives the request, it removes the existing data from the +Recommendation Clusters. In case of an error, the entire request is rejected +and the existing state is maintained. > [!NOTE] > **Note:** This api is available from version 1.1.0 onwards. @@ -712,7 +785,9 @@ This API is used to delete the content of Featured Cluster. client.deleteFeaturedCluster(); -When the service receives the request, it removes the existing data from the Featured Cluster. In case of an error, the entire request is rejected and the existing state is maintained. +When the service receives the request, it removes the existing data from the +Featured Cluster. In case of an error, the entire request is rejected +and the existing state is maintained. > [!NOTE] > **Note:** This api is available from version 1.1.0 onwards. @@ -732,7 +807,9 @@ This API is used to delete the content of Continuation Cluster. client.deleteContinuationCluster(); -When the service receives the request, it removes the existing data from the Continuation Cluster. In case of an error, the entire request is rejected and the existing state is maintained. +When the service receives the request, it removes the existing data from the +Continuation Cluster. In case of an error, the entire request is rejected +and the existing state is maintained. > [!NOTE] > **Note:** This api is available from version 1.1.0 onwards. @@ -752,7 +829,9 @@ This API is used to delete the content of UserAccountManagement Cluster. client.deleteUserManagementCluster(); -When the service receives the request, it removes the existing data from the UserAccountManagement Cluster. In case of an error, the entire request is rejected and the existing state is maintained. +When the service receives the request, it removes the existing data from the +UserAccountManagement Cluster. In case of an error, the entire request is +rejected and the existing state is maintained. > [!NOTE] > **Note:** This api is available from version 1.1.0 onwards. @@ -782,11 +861,15 @@ This API is used to delete the content of a given cluster type. ... .build()); -When the service receives the request, it removes the existing data from all clusters matching the specified cluster types. Clients can choose to pass one or many cluster types. In case of an error, the entire request is rejected and the existing state is maintained. +When the service receives the request, it removes the existing data from all +clusters matching the specified cluster types. Clients can choose to pass one or +many cluster types. In case of an error, the entire request is rejected and the +existing state is maintained. #### Error handling -It is highly recommended to listen to the task result from the publish APIs such that a follow-up action can be taken to recover and resubmit an successful task. +It is highly recommended to listen to the task result from the publish APIs such +that a follow-up action can be taken to recover and resubmit an successful task. client.publishRecommendationClusters( new PublishRecommendationClustersRequest.Builder() @@ -808,7 +891,8 @@ It is highly recommended to listen to the task result from the publish APIs such } }); -The error is returned as an `AppEngageException` with the cause included as an error code. +The error is returned as an `AppEngageException` with the cause included as an +error code. | Error code | Error name | Note | |---|---|---| @@ -822,13 +906,23 @@ The error is returned as an `AppEngageException` with the cause included as an e ### Step 3: Handle broadcast intents -In addition to making publish content API calls through a job, it is also required to set up a [`BroadcastReceiver`](https://developer.android.com/reference/android/content/BroadcastReceiver) to receive the request for a content publish. +In addition to making publish content API calls through a job, it is also +required to set up a +[`BroadcastReceiver`](https://developer.android.com/reference/android/content/BroadcastReceiver) to receive +the request for a content publish. -The goal of broadcast intents is mainly for app reactivation and forcing data sync. Broadcast intents are not designed to be sent very frequently. It is only triggered when the Engage Service determines the content might be stale (for example, a week old). That way, there is more confidence that the user can have a fresh content experience, even if the application has not been executed for a long period of time. +The goal of broadcast intents is mainly for app reactivation and forcing data +sync. Broadcast intents are not designed to be sent very frequently. It is only +triggered when the Engage Service determines the content might be stale (for +example, a week old). That way, there is more confidence that the user can have +a fresh content experience, even if the application has not been executed for a +long period of time. The `BroadcastReceiver` must be set up in the following two ways: -- Dynamically register an instance of the `BroadcastReceiver` class using `Context.registerReceiver()`. This enables communication from applications that are still live in memory. +- Dynamically register an instance of the `BroadcastReceiver` class using + `Context.registerReceiver()`. This enables communication from applications + that are still live in memory. ### Kotlin @@ -899,7 +993,10 @@ The `BroadcastReceiver` must be set up in the following two ways: } -- Statically declare an implementation with the `` tag in your `AndroidManifest.xml` file. This allows the application to receive broadcast intents when it is not running, and also allows the application to publish the content. +- Statically declare an implementation with the `` tag in your + `AndroidManifest.xml` file. This allows the application to receive broadcast + intents when it is not running, and also allows the application to publish + the content. -The following [intents](https://developer.android.com/reference/android/content/Intent) will be sent by the service: +The following [intents](https://developer.android.com/reference/android/content/Intent) will be sent by the +service: - `com.google.android.engage.action.PUBLISH_RECOMMENDATION` It is recommended to start a `publishRecommendationClusters` call when receiving this intent. - `com.google.android.engage.action.PUBLISH_FEATURED` It is recommended to start a `publishFeaturedCluster` call when receiving this intent. @@ -927,15 +1025,20 @@ The following [intents](https://developer.android.com/reference/android/content/ ## Integration workflow -For a step-by-step guide on verifying your integration after it is complete, see [Engage developer integration workflow](https://developer.android.com/guide/playcore/engage/workflow). +For a step-by-step guide on verifying your integration after it is complete, see +[Engage developer integration workflow](https://developer.android.com/guide/playcore/engage/workflow). ## FAQs -See [Engage SDK Frequently Asked Questions](https://developer.android.com/guide/playcore/engage/faq) for FAQs. +See [Engage SDK Frequently Asked Questions](https://developer.android.com/guide/playcore/engage/faq) for +FAQs. ## Contact -Contact [`engage-developers@google.com`](mailto:engage-developers@google.com) if there are any questions during the integration process. Our team will reply as soon as possible. +Contact +[`engage-developers@google.com`](mailto:engage-developers@google.com) if there are +any questions during the integration process. Our team will reply as soon as +possible. ## Next steps diff --git a/play/engage-sdk-integration/references/android/guide/playcore/engage/otherverticals.md b/play/engage-sdk-integration/references/android/guide/playcore/engage/otherverticals.md index 67463e0..b9a6d61 100644 --- a/play/engage-sdk-integration/references/android/guide/playcore/engage/otherverticals.md +++ b/play/engage-sdk-integration/references/android/guide/playcore/engage/otherverticals.md @@ -1,43 +1,86 @@ -Boost app engagement by reaching your users where they are. Integrate Engage SDK to deliver personalized recommendations and continuation content directly to users across multiple on-device surfaces, like **[Collections](https://android-developers.googleblog.com/2024/07/introducing-collections-powered-by-engage-sdk.html)** , **[Entertainment Space](https://blog.google/products/android/entertainment-space/)** , and the Play Store. The integration adds less than 50 KB (compressed) to the average APK and takes most apps about a week of developer time. Learn more at our **[business site](http://play.google.com/console/about/programs/EngageSDK)**. +Boost app engagement by reaching your users where they are. Integrate Engage SDK +to deliver personalized recommendations and continuation content directly to +users across multiple on-device surfaces, like +**[Collections](https://android-developers.googleblog.com/2024/07/introducing-collections-powered-by-engage-sdk.html)** , **[Entertainment +Space](https://blog.google/products/android/entertainment-space/)** , and the Play Store. The integration adds +less than 50 KB (compressed) to the average APK and takes most apps about a +week of developer time. Learn more at our **[business +site](http://play.google.com/console/about/programs/EngageSDK)**. -This document contains instructions for developer partners to integrate new content like reservations, events, lodging, places of interest, people and other content that might not belong to any of these categories. +This document contains instructions for developer partners to integrate new +content like reservations, events, lodging, places of interest, people and other +content that might not belong to any of these categories. ## Integration detail ### Terminology -This integration includes the following three cluster types: **Recommendation** , **Featured** , and **Continuation**. +This integration includes the following three cluster types: **Recommendation** , +**Featured** , and **Continuation**. -- **Recommendation** clusters show personalized suggestions from an individual developer partner. It is a UI view that contains a group of recommendations from the same developer partner. +- **Recommendation** clusters show personalized suggestions from an individual + developer partner. It is a UI view that contains a group of recommendations + from the same developer partner. - - **ArticleEntity**: ArticleEntity that represents a text-based recommendation for content that is relevant to more than one category of content. ArticleEntity item allows developers to provide a variety of text and image content with more metadata to articulate the information to the users compared to GenericFeaturedEntity. Ex: Marketing content, News snippet + - **ArticleEntity**: ArticleEntity that represents a text-based + recommendation for content that is relevant to more than one category + of content. ArticleEntity item allows developers to provide a variety of + text and image content with more metadata to articulate the information + to the users compared to GenericFeaturedEntity. Ex: Marketing content, + News snippet ![](https://developer.android.com/static/images/guide/playcore/engage/article-entity-travel.png) **Figure 1:** UI showing a single ArticleEntity within Recommendations cluster. - - **EventEntity**: EventEntity represents an event happening in the future. Event start time is a critical piece of information that needs to be conveyed to the users. + - **EventEntity**: EventEntity represents an event happening in the + future. Event start time is a critical piece of information that + needs to be conveyed to the users. ![](https://developer.android.com/static/images/guide/playcore/engage/event-entity-travel.png) **Figure 2:** UI showing a single EventEntity within Recommendations cluster. - - **LodgingEntity**: LodgingEntity represents an accommodation, such as a hotel, apartment, vacation home for short term and long term rental. + - **LodgingEntity**: LodgingEntity represents an accommodation, such + as a hotel, apartment, vacation home for short term and long term + rental. ![](https://developer.android.com/static/images/guide/playcore/engage/lodging-entity-travel.png) **Figure 3:** UI showing a single LodgingEntity within Recommendations cluster. - - **StoreEntity**: StoreEntity represents a store, restaurant, cafe etc. It highlights content where a dining venue or store is the critical piece of information that needs to be conveyed to the users. + - **StoreEntity**: StoreEntity represents a store, restaurant, cafe + etc. It highlights content where a dining venue or store is the + critical piece of information that needs to be conveyed to the + users. ![](https://developer.android.com/static/images/guide/playcore/engage/store-entity-travel.png) **Figure 4:** UI showing a single StoreEntity within Recommendations cluster. - - **PointOfInterestEntity**: PointOfInterestEntity represents a place of interest like, a gas station, event venue, theme park, museum, tourist attraction, hiking trail etc. It highlights content where location is a critical piece of information that needs to be conveyed to the users. It shouldn't be used for lodging, a store or a dining venue. + - **PointOfInterestEntity**: PointOfInterestEntity represents a + place of interest like, a gas station, event venue, theme park, + museum, tourist attraction, hiking trail etc. It highlights content + where location is a critical piece of information that needs to be + conveyed to the users. It shouldn't be used for lodging, a store or + a dining venue. ![](https://developer.android.com/static/images/guide/playcore/engage/poi-entity-travel.png) **Figure 5:** UI showing a single PointOfInterestEntity within Recommendations cluster. - - **PersonEntity**: PersonEntity represents a person. The recommendations could be to highlight a person in categories like health and fitness, sports, dating, etc. + - **PersonEntity**: PersonEntity represents a person. The recommendations + could be to highlight a person in categories like health and fitness, + sports, dating, etc. ![](https://developer.android.com/static/images/guide/playcore/engage/person-entity-dating.png) **Figure 5:** UI showing a single PersonEntity within Recommendations cluster. -- The **Continuation** cluster shows content recently engaged by users from multiple developer partners in a single UI grouping. Each developer partner will be allowed to broadcast a maximum of 10 entities in the Continuation cluster. +- The **Continuation** cluster shows content recently engaged by users from + multiple developer partners in a single UI grouping. Each developer partner + will be allowed to broadcast a maximum of 10 entities in the Continuation + cluster. Your continuation content can take the following structure: - - **ArticleEntity**: ArticleEntity that represents a text-based recommendation for content that is relevant to more than one category of content. This entity can be used to represent unfinished news articles or other content that the user would like to continue consuming from where they left it. Ex: Marketing content, News snippet + - **ArticleEntity**: ArticleEntity that represents a text-based + recommendation for content that is relevant to more than one category + of content. This entity can be used to represent unfinished news + articles or other content that the user would like to continue consuming + from where they left it. Ex: Marketing content, News snippet ![](https://developer.android.com/static/images/guide/playcore/engage/article-entity-continuation-travel.png) **Figure 6.** UI showing a single ArticleEntity within a Continuation cluster. - - **RestaurantReservationEntity**: RestaurantReservationEntity represents a reservation for a restaurant or cafe and helps users track upcoming or ongoing restaurant reservations. + - **RestaurantReservationEntity**: RestaurantReservationEntity represents + a reservation for a restaurant or cafe and helps users track upcoming or + ongoing restaurant reservations. ![](https://developer.android.com/static/images/guide/playcore/engage/restaurant-reservation-entity-travel.png) **Figure 7.** UI showing a single RestaurantReservationEntity within a Continuation cluster. - - **EventReservationEntity**: EventReservationEntity represents a reservation for an event and helps users track upcoming or ongoing events reservations. Events could include, but not limited to the following: + - **EventReservationEntity**: EventReservationEntity represents a + reservation for an event and helps users track upcoming or ongoing + events reservations. Events could include, but not limited to the + following: - Sports events like reservation for a football match - Gaming events like reservation for eSports @@ -47,18 +90,34 @@ This integration includes the following three cluster types: **Recommendation** - Education / Training sessions reservations ![](https://developer.android.com/static/images/guide/playcore/engage/event-reservation-entity-travel.png) **Figure 8.** UI showing a single EventReservationEntity within a Continuation cluster. - - **LodgingReservationEntity**: LodgingEntityReservation represents a reservation for a travel lodging and helps users track upcoming or ongoing hotel or vacation rental reservations. + - **LodgingReservationEntity**: LodgingEntityReservation represents a + reservation for a travel lodging and helps users track upcoming or + ongoing hotel or vacation rental reservations. ![](https://developer.android.com/static/images/guide/playcore/engage/lodging-reservation-entity-travel.png) **Figure 9.** UI showing a single LodgingReservationEntity within a Continuation cluster. - - **TransportationReservationEntity**: TransportationReservationEntity represents a reservation for transportation by any mode and helps users track reservations for upcoming or ongoing flight, ferry, train, bus, ride-hailing, or cruise. + - **TransportationReservationEntity**: TransportationReservationEntity + represents a reservation for transportation by any mode and helps users + track reservations for upcoming or ongoing flight, ferry, train, bus, + ride-hailing, or cruise. ![](https://developer.android.com/static/images/guide/playcore/engage/transportation-reservation-entity-travel.png) **Figure 10.** UI showing a single TransportationReservationEntity within a Continuation cluster. - - **VehicleRentalReservationEntity**: VehicleRentalReservationEntity represents a vehicle rental reservation and helps users track upcoming or ongoing vehicle rental reservations. + - **VehicleRentalReservationEntity**: VehicleRentalReservationEntity + represents a vehicle rental reservation and helps users track upcoming + or ongoing vehicle rental reservations. ![](https://developer.android.com/static/images/guide/playcore/engage/vehicle-rental-reservation-entity-travel.png) **Figure 11.** UI showing a single VehicleRentalReservationEntity within a Continuation cluster. -- The **Featured** cluster is a UI view that showcases the chosen hero `GenericFeaturedEntity` from many developer partners in one UI grouping. There is a single Featured cluster, which is surfaced near the top of the UI, with a priority placement above all Recommendation clusters. Each developer partner is allowed to broadcast a single entity of a supported type in Featured, with many entities (potentially of different types) from multiple app developers in the Featured cluster. +- The **Featured** cluster is a UI view that showcases the chosen hero + `GenericFeaturedEntity` from many developer partners in one UI grouping. + There is a single Featured cluster, which is surfaced near the top of the + UI, with a priority placement above all Recommendation clusters. Each + developer partner is allowed to broadcast a single entity of a supported + type in Featured, with many entities (potentially of different types) from + multiple app developers in the Featured cluster. - - **GenericFeaturedEntity**: GenericFeaturedEntity differs from Recommendation item in that Featured item should be used for a single top content from developers and should represent the single most important content that will be interesting and relevant to users. + - **GenericFeaturedEntity**: GenericFeaturedEntity differs from + Recommendation item in that Featured item should be used for a single + top content from developers and should represent the single most + important content that will be interesting and relevant to users. ![](https://developer.android.com/static/images/guide/playcore/engage/featured-item-health-and-fitness.png) **Figure 12:** UI showing a single hero GenericFeaturedEntity card within a Featured cluster @@ -75,9 +134,11 @@ Add the `com.google.android.engage:engage-core` library to your app: ### Summary -The design is based on an implementation of a [bound service](https://developer.android.com/guide/components/bound-services). +The design is based on an implementation of a +[bound service](https://developer.android.com/guide/components/bound-services). -The data a client can publish is subject to the following limits for different cluster types: +The data a client can publish is subject to the following limits for different +cluster types: | Cluster type | Cluster limits | Minimum entity limits in a cluster | Maximum entity limits in a cluster | |---|---|---|---| @@ -87,7 +148,8 @@ The data a client can publish is subject to the following limits for different c ### Step 1: Provide entity data -The SDK has defined different entities to represent each item type. We support the following entities for the others category: +The SDK has defined different entities to represent each item type. We support +the following entities for the others category: 1. `GenericFeaturedEntity` 2. `ArticleEntity` @@ -195,7 +257,8 @@ The charts below outline available attributes and requirements for each type. #### `StoreEntity` -The `StoreEntity` object represents an individual store that developer partners want to publish, such as a restaurant or a grocery store. +The `StoreEntity` object represents an individual store that developer partners +want to publish, such as a restaurant or a grocery store. | Attribute | Requirement | Description | Format | |---|---|---|---| @@ -430,7 +493,8 @@ Required specifications for image assets are listed in this table: | Landscape (1.91x1) | 600x314 | 1200x628 | | Portrait (4x5) | 480x600 | 960x1200 | -The images are required to be hosted on public CDNs so that Google can access them. +The images are required to be hosted on public CDNs so that Google can access +them. *File formats* @@ -447,7 +511,8 @@ PNG, JPG, static GIF, WebP #### Content Category -The content category allows apps to publish content belonging to multiple categories. This maps the content with some of the predefined categories namely: +The content category allows apps to publish content belonging to multiple +categories. This maps the content with some of the predefined categories namely: - `TYPE_EDUCATION` - `TYPE_SPORTS` @@ -467,25 +532,33 @@ The content category allows apps to publish content belonging to multiple catego - `TYPE_PARENTING` - `TYPE_DATING` -The images are required to be hosted on public CDNs so that Google can access them. +The images are required to be hosted on public CDNs so that Google can access +them. *Guidelines to use the content categories* 1. Some entities like **ArticleEntity** and **GenericFeaturedEntity** are eligible to use any of the content categories. For other entities like **EventEntity** , **EventReservationEntity** , **PointOfInterestEntity**, only a subset of these categories are eligible. Check the list of categories eligible for an entity type before populating the list. -2. Use the specific entity type for some content categories over a combination of the Generic entities and the ContentCategory: +2. Use the specific entity type for some content categories over a combination + of the Generic entities and the ContentCategory: - TYPE_MOVIES_AND_TV_SHOWS - Check out the entities from [Watch integration guide](https://developer.android.com/guide/playcore/engage/watch) before using the generic entities. - TYPE_BOOKS - Check out the [EbookEntity](https://developer.android.com/guide/playcore/engage/read#ebookentity) before using the generic entities. - TYPE_AUDIOBOOKS - Check out [AudiobookEntity](https://developer.android.com/guide/playcore/engage/read#audiobookentity) before using the generic entities. - TYPE_SHOPPING - Check out [ShoppingEntity](https://developer.android.com/guide/playcore/engage/shopping#shoppingEntity) before using the generic entities. - TYPE_FOOD_AND_DRINK - Check out entities from [Food Integration guide](https://developer.android.com/guide/playcore/engage/food) before using the generic entities. -3. The ContentCategory field is optional and should be left blank if the content doesn't belong to any of the categories mentioned earlier. +3. The ContentCategory field is optional and should be left blank if the + content doesn't belong to any of the categories mentioned earlier. -4. In case multiple content categories are provided, provide them in the order of relevance to the content with the most relevant content category placed first in the list. +4. In case multiple content categories are provided, provide them in the order + of relevance to the content with the most relevant content category placed + first in the list. ### Step 2: Provide Cluster data -It is recommended to have the content publish job executed in the background (for example, using [WorkManager](https://developer.android.com/topic/libraries/architecture/workmanager)) and scheduled on a regular basis or on an event basis (for example, every time the user opens the app or when the user just added something to their cart). +It is recommended to have the content publish job executed in the background +(for example, using [WorkManager](https://developer.android.com/topic/libraries/architecture/workmanager)) +and scheduled on a regular basis or on an event basis (for example, every time +the user opens the app or when the user just added something to their cart). `AppEngagePublishClient` is responsible for publishing clusters. @@ -505,11 +578,11 @@ There are following APIs to publish clusters in the client: #### `isServiceAvailable` -This API is used to check if the service is available for integration and whether the content can be presented on the device. +This API is used to check if the service is available for integration and +whether the content can be presented on the device. ##### For Engage SDK v1.6.0 and higher (Recommended) -
## Android skills @@ -527,7 +600,11 @@ If your team uses AI coding tools (such as Gemini in Android Studio), you can au
-You can check the service availability for every cluster type that you intend to publish. The `isServiceAvailable` API accepts a request object, `ServiceAvailabilityRequest`, which contains the cluster types for which service availability needs to be checked. You can find the `ClusterType` enum values required for `ServiceAvailabilityRequest` from the following table. +You can check the service availability for every cluster type that you intend to +publish. The `isServiceAvailable` API accepts a request object, +`ServiceAvailabilityRequest`, which contains the cluster types for which service +availability needs to be checked. You can find the `ClusterType` enum values +required for `ServiceAvailabilityRequest` from the following table. | Cluster Type | Cluster Type Constant | Integer Value | |---|---|---| @@ -594,11 +671,26 @@ You can check the service availability for every cluster type that you intend to ###### Conditional Service Availability Feature -Some integrated apps request a special configuration that enables and disables the Engage service intermittently in order to reduce their serving cost. This intermittent content ingestion strategy, although possible, negatively affects the user and the product -- stale content will not be presented and some surfaces will not be served at all. +Some integrated apps request a special configuration that enables and disables +the Engage service intermittently in order to reduce their serving cost. This +intermittent content ingestion strategy, although possible, negatively affects +the user and the product -- stale content will not be presented and some surfaces +will not be served at all. -Starting with v1.6.0, the Engage SDK allows checking availability for specific cluster types. This provides more flexibility so that if the intermittent content strategy was adopted by a given application, some cluster types can follow that intermittent strategy while other cluster types are always enabled (i.e. continuation clusters). +Starting with v1.6.0, the Engage SDK allows checking availability for specific +cluster types. This provides more flexibility so that if the intermittent +content strategy was adopted by a given application, some cluster types can +follow that intermittent strategy while other cluster types are always enabled +(i.e. continuation clusters). -If the Engage service should not be 'continuously' enabled on all supported devices for whatever reason, and is configured for intermittent ingestion for any set of devices, all continuation cluster publications (e.g. Continue Reading and Reservations) will be still enabled by default configuration, and the rest of the cluster types will be enabled and disabled intermittently. If intermittent ingestion applies to you but this default configuration is not suitable for your needs, please contact engage-developers@google.com. +If the Engage service should not be 'continuously' enabled on all supported +devices for whatever reason, and is configured for intermittent ingestion for +any set of devices, all continuation cluster publications (e.g. Continue +Reading and Reservations) will be still enabled by default configuration, +and the rest of the cluster types will be enabled and disabled +intermittently. If intermittent ingestion applies to you but this default +configuration is not suitable for your needs, please contact +engage-developers@google.com. ##### For SDK versions prior to v1.6.0 (Deprecated) @@ -672,12 +764,14 @@ This API is used to publish a list of `RecommendationCluster` objects. .build()) .build()); -When the service receives the request, the following actions take place within one transaction: +When the service receives the request, the following actions take place within +one transaction: - Existing `RecommendationCluster` data from the developer partner is removed. - Data from the request is parsed and stored in the updated Recommendation Cluster. -In case of an error, the entire request is rejected and the existing state is maintained. +In case of an error, the entire request is rejected and the existing state is +maintained. #### `publishFeaturedCluster` @@ -708,12 +802,14 @@ This API is used to publish a list of `FeaturedCluster` objects. .build()) .build()); -When the service receives the request, the following actions take place within one transaction: +When the service receives the request, the following actions take place within +one transaction: - Existing `FeaturedCluster` data from the developer partner is removed. - Data from the request is parsed and stored in the updated Featured Cluster. -In case of an error, the entire request is rejected and the existing state is maintained. +In case of an error, the entire request is rejected and the existing state is +maintained. #### `publishContinuationCluster` @@ -744,16 +840,20 @@ This API is used to publish a `ContinuationCluster` object. .build()) .build()); -When the service receives the request, the following actions take place within one transaction: +When the service receives the request, the following actions take place within +one transaction: - Existing `ContinuationCluster` data from the developer partner is removed. - Data from the request is parsed and stored in the updated Continuation Cluster. -In case of an error, the entire request is rejected and the existing state is maintained. +In case of an error, the entire request is rejected and the existing state is +maintained. #### `publishUserAccountManagementRequest` -This API is used to publish a Sign In card . The signin action directs users to the app's sign in page so that the app can publish content (or provide more personalized content) +This API is used to publish a Sign In card . The signin action directs users to +the app's sign in page so that the app can publish content (or provide more +personalized content) The following metadata is part of the Sign In Card - @@ -806,16 +906,20 @@ The following metadata is part of the Sign In Card - .setSignInCardEntity(SIGN_IN_CARD_ENTITY) .build()); -When the service receives the request, the following actions take place within one transaction: +When the service receives the request, the following actions take place within +one transaction: - Existing `UserAccountManagementCluster` data from the developer partner is removed. - Data from the request is parsed and stored in the updated UserAccountManagementCluster Cluster. -In case of an error, the entire request is rejected and the existing state is maintained. +In case of an error, the entire request is rejected and the existing state is +maintained. #### `updatePublishStatus` -If for any internal business reason, none of the clusters is published, we **strongly recommend** updating the publish status using the **updatePublishStatus** API. This is important because : +If for any internal business reason, none of the clusters is published, we +**strongly recommend** updating the publish status using the +**updatePublishStatus** API. This is important because : - Providing the status in all scenarios, even when the content is published (STATUS == PUBLISHED), is critical to populate dashboards that use this explicit status to convey the health and other metrics of your integration. - If no content is published but the integration status isn't broken (STATUS == NOT_PUBLISHED), Google can avoid triggering alerts in the app health dashboards. It confirms that content is not published due to an **expected** situation from the provider's standpoint. @@ -855,7 +959,11 @@ The list of eligible publish status codes are : // Reach out to engage-developers@ before using this enum. AppEngagePublishStatusCode.NOT_PUBLISHED_OTHER -If the content is not published due to a user not logged in, Google would recommend publishing the Sign In Card. If for any reason providers are not able to publish the Sign In Card then we recommend calling the **updatePublishStatus** API with the status code **NOT_PUBLISHED_REQUIRES_SIGN_IN** +If the content is not published due to a user not logged in, Google would +recommend publishing the Sign In Card. If for any reason providers are not able +to publish the Sign In Card then we recommend calling the +**updatePublishStatus** API with the status code +**NOT_PUBLISHED_REQUIRES_SIGN_IN** ### Kotlin @@ -886,7 +994,9 @@ This API is used to delete the content of Recommendation Clusters. client.deleteRecommendationClusters(); -When the service receives the request, it removes the existing data from the Recommendation Clusters. In case of an error, the entire request is rejected and the existing state is maintained. +When the service receives the request, it removes the existing data from the +Recommendation Clusters. In case of an error, the entire request is rejected and +the existing state is maintained. > [!NOTE] > **Note:** This api is available from version 1.1.0 onwards. @@ -906,7 +1016,9 @@ This API is used to delete the content of Featured Cluster. client.deleteFeaturedCluster(); -When the service receives the request, it removes the existing data from the Featured Cluster. In case of an error, the entire request is rejected and the existing state is maintained. +When the service receives the request, it removes the existing data from the +Featured Cluster. In case of an error, the entire request is rejected and the +existing state is maintained. > [!NOTE] > **Note:** This api is available from version 1.1.0 onwards. @@ -926,7 +1038,9 @@ This API is used to delete the content of Continuation Cluster. client.deleteContinuationCluster(); -When the service receives the request, it removes the existing data from the Continuation Cluster. In case of an error, the entire request is rejected and the existing state is maintained. +When the service receives the request, it removes the existing data from the +Continuation Cluster. In case of an error, the entire request is rejected and +the existing state is maintained. > [!NOTE] > **Note:** This api is available from version 1.1.0 onwards. @@ -946,7 +1060,9 @@ This API is used to delete the content of UserAccountManagement Cluster. client.deleteUserManagementCluster(); -When the service receives the request, it removes the existing data from the UserAccountManagement Cluster. In case of an error, the entire request is rejected and the existing state is maintained. +When the service receives the request, it removes the existing data from the +UserAccountManagement Cluster. In case of an error, the entire request is +rejected and the existing state is maintained. > [!NOTE] > **Note:** This api is available from version 1.1.0 onwards. @@ -976,11 +1092,15 @@ This API is used to delete the content of a given cluster type. .addClusterType(ClusterType.TYPE_RECOMMENDATION) .build()); -When the service receives the request, it removes the existing data from all clusters matching the specified cluster types. Clients can choose to pass one or many cluster types. In case of an error, the entire request is rejected and the existing state is maintained. +When the service receives the request, it removes the existing data from all +clusters matching the specified cluster types. Clients can choose to pass one or +many cluster types. In case of an error, the entire request is rejected and the +existing state is maintained. #### Error handling -It is highly recommended to listen to the task result from the publish APIs such that a follow-up action can be taken to recover and resubmit an successful task. +It is highly recommended to listen to the task result from the publish APIs such +that a follow-up action can be taken to recover and resubmit an successful task. ### Kotlin @@ -1024,7 +1144,8 @@ It is highly recommended to listen to the task result from the publish APIs such } }); -The error is returned as an `AppEngageException` with the cause included as an error code. +The error is returned as an `AppEngageException` with the cause included as an +error code. | Error code | Error name | Note | |---|---|---| @@ -1038,13 +1159,23 @@ The error is returned as an `AppEngageException` with the cause included as an e ### Step 3: Handle broadcast intents -In addition to making publish content API calls through a job, it is also required to set up a [`BroadcastReceiver`](https://developer.android.com/reference/android/content/BroadcastReceiver) to receive the request for a content publish. +In addition to making publish content API calls through a job, it is also +required to set up a +[`BroadcastReceiver`](https://developer.android.com/reference/android/content/BroadcastReceiver) to receive +the request for a content publish. -The goal of broadcast intents is mainly for app reactivation and forcing data sync. Broadcast intents are not designed to be sent very frequently. It is only triggered when the Engage Service determines the content might be stale (for example, a week old). That way, there is more confidence that the user can have a fresh content experience, even if the application has not been executed for a long period of time. +The goal of broadcast intents is mainly for app reactivation and forcing data +sync. Broadcast intents are not designed to be sent very frequently. It is only +triggered when the Engage Service determines the content might be stale (for +example, a week old). That way, there is more confidence that the user can have +a fresh content experience, even if the application has not been executed for a +long period of time. The `BroadcastReceiver` must be set up in the following two ways: -- Dynamically register an instance of the `BroadcastReceiver` class using `Context.registerReceiver()`. This enables communication from applications that are still live in memory. +- Dynamically register an instance of the `BroadcastReceiver` class using + `Context.registerReceiver()`. This enables communication from applications + that are still live in memory. ### Kotlin @@ -1114,7 +1245,10 @@ The `BroadcastReceiver` must be set up in the following two ways: /*scheduler=*/null); } -- Statically declare an implementation with the `` tag in your `AndroidManifest.xml` file. This allows the application to receive broadcast intents when it is not running, and also allows the application to publish the content. +- Statically declare an implementation with the `` tag in your + `AndroidManifest.xml` file. This allows the application to receive broadcast + intents when it is not running, and also allows the application to publish + the content. -The following [intents](https://developer.android.com/reference/android/content/Intent) is sent by the service: +The following [intents](https://developer.android.com/reference/android/content/Intent) is sent by the +service: - `com.google.android.engage.action.PUBLISH_RECOMMENDATION` It is recommended to start a `publishRecommendationClusters` call when receiving this intent. - `com.google.android.engage.action.PUBLISH_FEATURED` It is recommended to start a `publishFeaturedCluster` call when receiving this intent. @@ -1142,15 +1277,19 @@ The following [intents](https://developer.android.com/reference/android/content/ ## Integration workflow -For a step-by-step guide on verifying your integration after it is complete, see [Engage developer integration workflow](https://developer.android.com/guide/playcore/engage/workflow). +For a step-by-step guide on verifying your integration after it is complete, see +[Engage developer integration workflow](https://developer.android.com/guide/playcore/engage/workflow). ## FAQs -See [Engage SDK Frequently Asked Questions](https://developer.android.com/guide/playcore/engage/faq) for FAQs. +See [Engage SDK Frequently Asked Questions](https://developer.android.com/guide/playcore/engage/faq) for +FAQs. ## Contact -Contact [`engage-developers@google.com`](mailto:engage-developers@google.com) if there are any questions during the integration process. +Contact +[`engage-developers@google.com`](mailto:engage-developers@google.com) if there are +any questions during the integration process. ## Next steps diff --git a/play/engage-sdk-integration/references/android/guide/playcore/engage/read.md b/play/engage-sdk-integration/references/android/guide/playcore/engage/read.md index bb51a5f..ab0650a 100644 --- a/play/engage-sdk-integration/references/android/guide/playcore/engage/read.md +++ b/play/engage-sdk-integration/references/android/guide/playcore/engage/read.md @@ -1,29 +1,49 @@ > [!IMPORTANT] > **Important:** Engage SDK has superseded Media Home, which is now deprecated. If you have an existing Media Home integration, follow the instructions in these guides to migrate your content to Engage SDK, which allows your content to be published to more devices and form factors. Please contact [`engage-developers@google.com`](mailto:engage-developers@google.com) if you have any questions. -Boost app engagement by reaching your users where they are. Integrate Engage SDK to deliver personalized recommendations and continuation content directly to users across multiple on-device surfaces, like **[Collections](https://android-developers.googleblog.com/2024/07/introducing-collections-powered-by-engage-sdk.html)** , **[Entertainment Space](https://blog.google/products/android/entertainment-space/)** , and the Play Store. The integration adds less than 50 KB (compressed) to the average APK and takes most apps about a week of developer time. Learn more at our **[business site](http://play.google.com/console/about/programs/EngageSDK)**. +Boost app engagement by reaching your users where they are. Integrate Engage SDK +to deliver personalized recommendations and continuation content directly to +users across multiple on-device surfaces, like +**[Collections](https://android-developers.googleblog.com/2024/07/introducing-collections-powered-by-engage-sdk.html)** , **[Entertainment +Space](https://blog.google/products/android/entertainment-space/)** , and the Play Store. The integration adds +less than 50 KB (compressed) to the average APK and takes most apps about a +week of developer time. Learn more at our **[business +site](http://play.google.com/console/about/programs/EngageSDK)**. -This guide contains instructions for developer partners to deliver reading content (eBooks, Audiobooks, Comics/Manga) to Engage content surfaces. +This guide contains instructions for developer partners to deliver reading +content (eBooks, Audiobooks, Comics/Manga) to Engage content surfaces. ## Integration detail ### Terminology -This integration includes the following three cluster types: **Recommendation** , **Continuation** , and **Featured**. +This integration includes the following three cluster types: **Recommendation** , +**Continuation** , and **Featured**. -- **Recommendation** clusters show personalized suggestions for content to read from an individual developer partner. +- **Recommendation** clusters show personalized suggestions for content to read + from an individual developer partner. Your recommendations take the following structure: - - **Recommendation Cluster:** A UI view that contains a group of recommendations from a single developer partner. + - **Recommendation Cluster:** A UI view that contains a group of + recommendations from a single developer partner. ![](https://developer.android.com/static/images/guide/playcore/engage/read-term-1.png) **Figure 1.** Entertainment Space UI showing a Recommendation Cluster from a single partner. - - **Entity:** An object representing a single item in a cluster. An entity can be an ebook, an audio book, a book series, and more. See the [Provide entity data](https://developer.android.com/guide/playcore/engage/read#provide-entity-data) section for a list of supported entity types. + - **Entity:** An object representing a single item in a cluster. An entity + can be an ebook, an audio book, a book series, and more. See the [Provide + entity data](https://developer.android.com/guide/playcore/engage/read#provide-entity-data) section for a list of supported entity + types. ![](https://developer.android.com/static/images/guide/playcore/engage/read-term-2.png) **Figure 2.** Entertainment Space UI showing a single Entity within a single partner's Recommendation Cluster. -- The **Continuation** cluster shows unfinished books from multiple developer partners in a single UI grouping. Each developer partner will be allowed to broadcast a maximum of 10 entities in the Continuation cluster. +- The **Continuation** cluster shows unfinished books from multiple developer + partners in a single UI grouping. Each developer partner will be allowed to + broadcast a maximum of 10 entities in the Continuation cluster. ![](https://developer.android.com/static/images/guide/playcore/engage/read-term-3.png) **Figure 3.** Entertainment Space UI showing a Continuation cluster with unfinished recommendations from multiple partners (only one recommendation is currently visible). -- The **Featured** cluster showcases a selection of items from multiple developer partners in a single UI grouping. There will be a single Featured cluster, which is surfaced near the top of the UI with a priority placement above all Recommendation clusters. Each developer partner will be allowed to broadcast up to 10 entities in the Featured cluster. +- The **Featured** cluster showcases a selection of items from multiple + developer partners in a single UI grouping. There will be a single Featured + cluster, which is surfaced near the top of the UI with a priority placement + above all Recommendation clusters. Each developer partner will be allowed to + broadcast up to 10 entities in the Featured cluster. ![](https://developer.android.com/static/images/guide/playcore/engage/read-term-4.png) **Figure 4.** Entertainment Space UI showing a Featured cluster with recommendations from multiple partners (only one recommendation is currently visible). @@ -40,9 +60,11 @@ Add the `com.google.android.engage:engage-core` library to your app: ### Summary -The design is based on an implementation of a [bound service](https://developer.android.com/guide/components/bound-services). +The design is based on an implementation of a [bound +service](https://developer.android.com/guide/components/bound-services). -The data a client can publish is subject to the following limits for different cluster types: +The data a client can publish is subject to the following limits for different +cluster types: | Cluster type | Cluster limits | Maximum entity limits in a cluster | |---|---|---| @@ -52,7 +74,8 @@ The data a client can publish is subject to the following limits for different c ### Step 1: Provide entity data -The SDK has defined different entities to represent each item type. We support the following entities for the Read category: +The SDK has defined different entities to represent each item type. We support +the following entities for the Read category: 1. `EbookEntity` 2. `AudiobookEntity` @@ -62,7 +85,8 @@ The charts below outline available attributes and requirements for each type. #### `EbookEntity` -The `EbookEntity` object represents an ebook (for example, the ebook of *Becoming* by Michelle Obama). +The `EbookEntity` object represents an ebook (for example, the ebook of +*Becoming* by Michelle Obama). | Attribute | Requirement | Notes | |---|---|---| @@ -86,7 +110,8 @@ The `EbookEntity` object represents an ebook (for example, the ebook of *Becomin #### `AudiobookEntity` -The `AudiobookEntity` object represents an audiobook (for example, the audiobook of *Becoming* by Michelle Obama). +The `AudiobookEntity` object represents an audiobook (for example, the audiobook +of *Becoming* by Michelle Obama). | Attribute | Requirement | Notes | |---|---|---| @@ -111,7 +136,8 @@ The `AudiobookEntity` object represents an audiobook (for example, the audiobook #### `BookSeriesEntity` -The `BookSeriesEntity` object represents a book series (for example, the *Harry Potter* book series, which has 7 books). +The `BookSeriesEntity` object represents a book series (for example, the *Harry +Potter* book series, which has 7 books). | Attribute | Requirement | Notes | |---|---|---| @@ -174,9 +200,13 @@ PNG, JPG, static GIF, WebP ### Step 2: Provide Cluster data -It's recommended to have the content publish job executed in the background (for example, using [WorkManager](https://developer.android.com/topic/libraries/architecture/workmanager)) and scheduled on a regular basis or on an event basis (for example, every time the user opens the app or when the user just added something to their cart). +It's recommended to have the content publish job executed in the background +(for example, using [WorkManager](https://developer.android.com/topic/libraries/architecture/workmanager)) +and scheduled on a regular basis or on an event basis (for example, every time +the user opens the app or when the user just added something to their cart). -`AppEngagePublishClient` is responsible for publishing clusters. Following APIs are available in the client: +`AppEngagePublishClient` is responsible for publishing clusters. Following +APIs are available in the client: - `isServiceAvailable` - `publishRecommendationClusters` @@ -192,11 +222,11 @@ It's recommended to have the content publish job executed in the background (for #### `isServiceAvailable` -This API is used to check if the service is available for integration and whether the content can be presented on the device. +This API is used to check if the service is available for integration and +whether the content can be presented on the device. ##### For Engage SDK v1.6.0 and higher (Recommended) -
## Android skills @@ -214,7 +244,11 @@ If your team uses AI coding tools (such as Gemini in Android Studio), you can au
-You can check the service availability for every cluster type that you intend to publish. The `isServiceAvailable` API accepts a request object, `ServiceAvailabilityRequest`, which contains the cluster types for which service availability needs to be checked. You can find the `ClusterType` enum values required for `ServiceAvailabilityRequest` from the following table. +You can check the service availability for every cluster type that you intend to +publish. The `isServiceAvailable` API accepts a request object, +`ServiceAvailabilityRequest`, which contains the cluster types for which service +availability needs to be checked. You can find the `ClusterType` enum values +required for `ServiceAvailabilityRequest` from the following table. | Cluster Type | Cluster Type Constant | Integer Value | |---|---|---| @@ -271,11 +305,26 @@ You can check the service availability for every cluster type that you intend to ###### Conditional Service Availability Feature -Some integrated apps request a special configuration that enables and disables the Engage service intermittently in order to reduce their serving cost. This intermittent content ingestion strategy, although possible, negatively affects the user and the product -- stale content will not be presented and some surfaces will not be served at all. +Some integrated apps request a special configuration that enables and disables +the Engage service intermittently in order to reduce their serving cost. This +intermittent content ingestion strategy, although possible, negatively affects +the user and the product -- stale content will not be presented and some surfaces +will not be served at all. -Starting with v1.6.0, the Engage SDK allows checking availability for specific cluster types. This provides more flexibility so that if the intermittent content strategy was adopted by a given application, some cluster types can follow that intermittent strategy while other cluster types are always enabled (i.e. continuation clusters). +Starting with v1.6.0, the Engage SDK allows checking availability for specific +cluster types. This provides more flexibility so that if the intermittent +content strategy was adopted by a given application, some cluster types can +follow that intermittent strategy while other cluster types are always enabled +(i.e. continuation clusters). -If the Engage service should not be 'continuously' enabled on all supported devices for whatever reason, and is configured for intermittent ingestion for any set of devices, all continuation cluster publications (e.g. Continue Reading) will be still enabled by default configuration, and the rest of the cluster types will be enabled and disabled intermittently. If intermittent ingestion applies to you but this default configuration is not suitable for your needs, please contact engage-developers@google.com. +If the Engage service should not be 'continuously' enabled on all supported +devices for whatever reason, and is configured for intermittent ingestion for +any set of devices, all continuation cluster publications (e.g. Continue +Reading) will be still enabled by default +configuration, and the rest of the cluster types will be enabled and disabled +intermittently. If intermittent ingestion applies to you but this default +configuration is not suitable for your needs, please contact +engage-developers@google.com. ##### For SDK versions prior to v1.6.0 (Deprecated) @@ -347,12 +396,14 @@ This API is used to publish a list of `RecommendationCluster` objects. .build()) .build()); -When the service receives the request, the following actions take place within one transaction: +When the service receives the request, the following actions take place within +one transaction: - Existing `RecommendationCluster` data from the developer partner is removed. - Data from the request is parsed and stored in the updated Recommendation Cluster. -In case of an error, the entire request is rejected and the existing state is maintained. +In case of an error, the entire request is rejected and the existing state is +maintained. #### `publishFeaturedCluster` @@ -381,12 +432,14 @@ This API is used to publish a list of `FeaturedCluster` objects. .build()) .build()); -When the service receives the request, the following actions take place within one transaction: +When the service receives the request, the following actions take place within +one transaction: - Existing `FeaturedCluster` data from the developer partner is removed. - Data from the request is parsed and stored in the updated Featured Cluster. -In case of an error, the entire request is rejected and the existing state is maintained. +In case of an error, the entire request is rejected and the existing state is +maintained. #### `publishContinuationCluster` @@ -417,16 +470,20 @@ This API is used to publish a `ContinuationCluster` object. .build()) .build()) -When the service receives the request, the following actions take place within one transaction: +When the service receives the request, the following actions take place within +one transaction: - Existing `ContinuationCluster` data from the developer partner is removed. - Data from the request is parsed and stored in the updated Continuation Cluster. -In case of an error, the entire request is rejected and the existing state is maintained. +In case of an error, the entire request is rejected and the existing state is +maintained. #### `publishUserAccountManagementRequest` -This API is used to publish a Sign In card . The signin action directs users to the app's sign in page so that the app can publish content (or provide more personalized content) +This API is used to publish a Sign In card . The signin action directs users to +the app's sign in page so that the app can publish content (or provide more +personalized content) The following metadata is part of the Sign In Card - @@ -479,16 +536,21 @@ The following metadata is part of the Sign In Card - .setSignInCardEntity(SIGN_IN_CARD_ENTITY) .build()); -When the service receives the request, the following actions take place within one transaction: +When the service receives the request, the following actions take place within +one transaction: - Existing `UserAccountManagementCluster` data from the developer partner is removed. - Data from the request is parsed and stored in the updated UserAccountManagementCluster Cluster. -In case of an error, the entire request is rejected and the existing state is maintained. +In case of an error, the entire request is rejected and the existing state is +maintained. #### `updatePublishStatus` -If for any internal business reason, none of the clusters is published, we **strongly recommend** updating the publish status using the **updatePublishStatus** API. This is important because : +If for any internal business reason, none of the clusters is published, +we **strongly recommend** updating the publish status using the +**updatePublishStatus** API. +This is important because : - Providing the status in all scenarios, even when the content is published (STATUS == PUBLISHED), is critical to populate dashboards that use this explicit status to convey the health and other metrics of your integration. - If no content is published but the integration status isn't broken (STATUS == NOT_PUBLISHED), Google can avoid triggering alerts in the app health dashboards. It confirms that content is not published due to an **expected** situation from the provider's standpoint. @@ -528,7 +590,11 @@ The list of eligible publish status codes are : // Reach out to engage-developers@ before using this enum. AppEngagePublishStatusCode.NOT_PUBLISHED_OTHER -If the content is not published due to a user not logged in, Google would recommend publishing the Sign In Card. If for any reason providers are not able to publish the Sign In Card then we recommend calling the **updatePublishStatus** API with the status code **NOT_PUBLISHED_REQUIRES_SIGN_IN** +If the content is not published due to a user not logged in, +Google would recommend publishing the Sign In Card. +If for any reason providers are not able to publish the Sign In Card +then we recommend calling the **updatePublishStatus** API +with the status code **NOT_PUBLISHED_REQUIRES_SIGN_IN** ### Kotlin @@ -559,7 +625,9 @@ This API is used to delete the content of Recommendation Clusters. client.deleteRecommendationClusters(); -When the service receives the request, it removes the existing data from the Recommendation Clusters. In case of an error, the entire request is rejected and the existing state is maintained. +When the service receives the request, it removes the existing data from the +Recommendation Clusters. In case of an error, the entire request is rejected +and the existing state is maintained. > [!NOTE] > **Note:** This api is available from version 1.1.0 onwards. @@ -579,7 +647,9 @@ This API is used to delete the content of Featured Cluster. client.deleteFeaturedCluster(); -When the service receives the request, it removes the existing data from the Featured Cluster. In case of an error, the entire request is rejected and the existing state is maintained. +When the service receives the request, it removes the existing data from the +Featured Cluster. In case of an error, the entire request is rejected +and the existing state is maintained. > [!NOTE] > **Note:** This api is available from version 1.1.0 onwards. @@ -599,7 +669,9 @@ This API is used to delete the content of Continuation Cluster. client.deleteContinuationCluster(); -When the service receives the request, it removes the existing data from the Continuation Cluster. In case of an error, the entire request is rejected and the existing state is maintained. +When the service receives the request, it removes the existing data from the +Continuation Cluster. In case of an error, the entire request is rejected +and the existing state is maintained. > [!NOTE] > **Note:** This api is available from version 1.1.0 onwards. @@ -619,7 +691,9 @@ This API is used to delete the content of UserAccountManagement Cluster. client.deleteUserManagementCluster(); -When the service receives the request, it removes the existing data from the UserAccountManagement Cluster. In case of an error, the entire request is rejected and the existing state is maintained. +When the service receives the request, it removes the existing data from the +UserAccountManagement Cluster. In case of an error, the entire request is +rejected and the existing state is maintained. > [!NOTE] > **Note:** This api is available from version 1.1.0 onwards. @@ -649,11 +723,15 @@ This API is used to delete the content of a given cluster type. ... .build()); -When the service receives the request, it removes the existing data from all clusters matching the specified cluster types. Clients can choose to pass one or many cluster types. In case of an error, the entire request is rejected and the existing state is maintained. +When the service receives the request, it removes the existing data from all +clusters matching the specified cluster types. Clients can choose to pass one or +many cluster types. In case of an error, the entire request is rejected and the +existing state is maintained. #### Error handling -It is highly recommended to listen to the task result from the publish APIs such that a follow-up action can be taken to recover and resubmit an successful task. +It is highly recommended to listen to the task result from the publish APIs such +that a follow-up action can be taken to recover and resubmit an successful task. client.publishRecommendationClusters( new PublishRecommendationClustersRequest.Builder() @@ -675,7 +753,8 @@ It is highly recommended to listen to the task result from the publish APIs such } }); -The error is returned as an `AppEngageException` with the cause included as an error code. +The error is returned as an `AppEngageException` with the cause included as an +error code. | Error code | Error name | Note | |---|---|---| @@ -689,13 +768,23 @@ The error is returned as an `AppEngageException` with the cause included as an e ### Step 3: Handle broadcast intents -In addition to making publish content API calls through a job, it is also required to set up a [`BroadcastReceiver`](https://developer.android.com/reference/android/content/BroadcastReceiver) to receive the request for a content publish. +In addition to making publish content API calls through a job, it is also +required to set up a +[`BroadcastReceiver`](https://developer.android.com/reference/android/content/BroadcastReceiver) to receive +the request for a content publish. -The goal of broadcast intents is mainly for app reactivation and forcing data sync. Broadcast intents are not designed to be sent very frequently. It is only triggered when the Engage Service determines the content might be stale (for example, a week old). That way, there is more confidence that the user can have a fresh content experience, even if the application has not been executed for a long period of time. +The goal of broadcast intents is mainly for app reactivation and forcing data +sync. Broadcast intents are not designed to be sent very frequently. It is only +triggered when the Engage Service determines the content might be stale (for +example, a week old). That way, there is more confidence that the user can have +a fresh content experience, even if the application has not been executed for a +long period of time. The `BroadcastReceiver` must be set up in the following two ways: -- Dynamically register an instance of the `BroadcastReceiver` class using `Context.registerReceiver()`. This enables communication from applications that are still live in memory. +- Dynamically register an instance of the `BroadcastReceiver` class using + `Context.registerReceiver()`. This enables communication from applications + that are still live in memory. ### Kotlin @@ -766,7 +855,10 @@ The `BroadcastReceiver` must be set up in the following two ways: } -- Statically declare an implementation with the `` tag in your `AndroidManifest.xml` file. This allows the application to receive broadcast intents when it is not running, and also allows the application to publish the content. +- Statically declare an implementation with the `` tag in your + `AndroidManifest.xml` file. This allows the application to receive broadcast + intents when it is not running, and also allows the application to publish + the content. -The following [intents](https://developer.android.com/reference/android/content/Intent) will be sent by the service: +The following [intents](https://developer.android.com/reference/android/content/Intent) will be sent by the +service: - `com.google.android.engage.action.PUBLISH_RECOMMENDATION` It is recommended to start a `publishRecommendationClusters` call when receiving this intent. - `com.google.android.engage.action.PUBLISH_FEATURED` It is recommended to start a `publishFeaturedCluster` call when receiving this intent. @@ -794,15 +887,18 @@ The following [intents](https://developer.android.com/reference/android/content/ ## Integration workflow -For a step-by-step guide on verifying your integration after it is complete, see [Engage developer integration workflow](https://developer.android.com/guide/playcore/engage/workflow). +For a step-by-step guide on verifying your integration after it is complete, see +[Engage developer integration workflow](https://developer.android.com/guide/playcore/engage/workflow). ## FAQs -See [Engage SDK Frequently Asked Questions](https://developer.android.com/guide/playcore/engage/faq) for FAQs. +See [Engage SDK Frequently Asked Questions](https://developer.android.com/guide/playcore/engage/faq) for +FAQs. ## Contact -Contact [`engage-developers@google.com`](mailto:engage-developers@google.com) if there are any questions during the integration process. Our team will reply as soon as possible. +Contact [`engage-developers@google.com`](mailto:engage-developers@google.com) if there are any questions during +the integration process. Our team will reply as soon as possible. ## Next steps diff --git a/play/engage-sdk-integration/references/android/guide/playcore/engage/shopping.md b/play/engage-sdk-integration/references/android/guide/playcore/engage/shopping.md index efc4644..c59de5f 100644 --- a/play/engage-sdk-integration/references/android/guide/playcore/engage/shopping.md +++ b/play/engage-sdk-integration/references/android/guide/playcore/engage/shopping.md @@ -1,40 +1,78 @@ -Boost app engagement by reaching your users where they are. Integrate Engage SDK to deliver personalized recommendations and continuation content directly to users across multiple on-device surfaces, like **[Collections](https://android-developers.googleblog.com/2024/07/introducing-collections-powered-by-engage-sdk.html)** , **[Entertainment Space](https://blog.google/products/android/entertainment-space/)** , and the Play Store. The integration adds less than 50 KB (compressed) to the average APK and takes most apps about a week of developer time. Learn more at our **[business site](http://play.google.com/console/about/programs/EngageSDK)**. +Boost app engagement by reaching your users where they are. Integrate Engage SDK +to deliver personalized recommendations and continuation content directly to +users across multiple on-device surfaces, like +**[Collections](https://android-developers.googleblog.com/2024/07/introducing-collections-powered-by-engage-sdk.html)** , **[Entertainment +Space](https://blog.google/products/android/entertainment-space/)** , and the Play Store. The integration adds +less than 50 KB (compressed) to the average APK and takes most apps about a +week of developer time. Learn more at our **[business +site](http://play.google.com/console/about/programs/EngageSDK)**. -This guide contains instructions for developer partners to deliver shopping content to Engage content surfaces. +This guide contains instructions for developer partners to deliver shopping +content to Engage content surfaces. ## Integration detail ### Terminology -This integration includes the following five cluster types: **Recommendation** , **Featured** , **Shopping Cart** , **Shopping List** , **Reorder** and **Shopping Order Tracking**. +This integration includes the following five cluster types: **Recommendation** , +**Featured** , **Shopping Cart** , **Shopping List** , **Reorder** and +**Shopping Order Tracking**. -- **Recommendation** clusters show personalized shopping suggestions from an individual developer partner. These recommendations can be personalized to the user or generalized (for example, trending items). Use these to surface products, events, sales, promos, subscriptions as you see fit. +- **Recommendation** clusters show personalized shopping suggestions from an + individual developer partner. These recommendations can be personalized to the + user or generalized (for example, trending items). Use these to surface + products, events, sales, promos, subscriptions as you see fit. Your recommendations take the following structure: - - **Recommendation Cluster:** A UI view that contains a group of recommendations from the same developer partner. + - **Recommendation Cluster:** A UI view that contains a group of + recommendations from the same developer partner. - **ShoppingEntity:** An object representing a single item in a cluster. -- The **Featured** cluster showcases a selection of entities from multiple developer partners in one UI grouping. There will be a single Featured cluster, which is surfaced near the top of the UI with a priority placement above all Recommendation clusters. Each developer partner will be allowed to broadcast up to 10 entities in the Featured cluster. +- The **Featured** cluster showcases a selection of entities from multiple + developer partners in one UI grouping. There will be a single Featured + cluster, which is surfaced near the top of the UI with a priority placement + above all Recommendation clusters. Each developer partner will be allowed to + broadcast up to 10 entities in the Featured cluster. -- The **Shopping Cart** cluster shows a sneak peek of shopping carts from many developer partners in one UI grouping, nudging users to complete their outstanding carts. There is a single Shopping Cart cluster, which is surfaced near the top of the UI, with a priority placement above all Recommendation clusters. Each developer partner is allowed to broadcast up to 3 `ShoppingCart` instances in the Shopping Cart cluster. +- The **Shopping Cart** cluster shows a sneak peek of shopping carts from many + developer partners in one UI grouping, nudging users to complete their + outstanding carts. There is a single Shopping Cart cluster, which is + surfaced near the top of the UI, with a priority placement above all + Recommendation clusters. Each developer partner is allowed to broadcast up to + 3 `ShoppingCart` instances in the Shopping Cart cluster. Your Shopping Cart takes the following structure: - - **Shopping Cart Cluster:** A UI view that contains a group of shopping cart previews from many developer partners. + - **Shopping Cart Cluster:** A UI view that contains a group of shopping + cart previews from many developer partners. - - **ShoppingCart:** An object representing the shopping cart preview for a single developer partner, to be displayed in the Shopping Cart cluster. The `ShoppingCart` must show the total count of items in the cart and may also include images for some items in the user's cart. + - **ShoppingCart:** An object representing the shopping cart preview + for a single developer partner, to be displayed in the Shopping Cart + cluster. The `ShoppingCart` must show the total count of items in the + cart and may also include images for some items in the user's cart. -- The **Shopping List** cluster shows a sneak peek of the shopping lists from multiple developer partners in one UI grouping, prompting users to return to the corresponding app to update and complete their lists. There is a single Shopping List cluster. +- The **Shopping List** cluster shows a sneak peek of the shopping + lists from multiple developer partners in one UI grouping, prompting users to + return to the corresponding app to update and complete their lists. There is a + single Shopping List cluster. -- The **Reorder** cluster shows a sneak peek of the previous orders from multiple developer partners in one UI grouping, prompting users to reorder. There is a single Reorder cluster. +- The **Reorder** cluster shows a sneak peek of the previous orders from + multiple developer partners in one UI grouping, prompting users to reorder. + There is a single Reorder cluster. - - Reorder cluster must show the total count of items in the user's previous order and must also include one of the following: + - Reorder cluster must show the total count of items in the + user's previous order and must also include one of the following: - Images for X items in the user's previous order. - Labels for X items in the user's previous order. -- The **Shopping Order Tracking** cluster shows a sneak peek of pending or recently completed shopping orders from many developer partners in one UI grouping, allowing users to track their orders. +- The **Shopping Order Tracking** cluster shows a sneak peek of pending + or recently completed shopping orders from many developer partners in one UI + grouping, allowing users to track their orders. - There is a single ShoppingOrderTracking cluster that is surfaced near the top of the UI, with a priority placement above all Recommendation clusters. Each developer partner is allowed to broadcast multiple ShoppingOrderTrackingEntity items in the Shopping Order Tracking cluster. + There is a single ShoppingOrderTracking cluster that is surfaced + near the top of the UI, with a priority placement above all Recommendation + clusters. Each developer partner is allowed to broadcast multiple + ShoppingOrderTrackingEntity items in the Shopping Order Tracking cluster. - Your ShoppingOrderTrackingCluster takes the following structure: - **ShoppingOrderTracking Cluster**: A UI view that contains a group of order tracking previews from many developer partners @@ -54,13 +92,16 @@ Add the `com.google.android.engage:engage-core` library to your app: implementation 'com.google.android.engage:engage-core:1.6.0' } -For more information, see [Package visibility in Android 11](https://developer.android.com/about/versions/11/privacy/package-visibility). +For more information, see [Package visibility in Android +11](https://developer.android.com/about/versions/11/privacy/package-visibility). ### Summary -The design is based on an implementation of a [bound service](https://developer.android.com/guide/components/bound-services). +The design is based on an implementation of a [bound +service](https://developer.android.com/guide/components/bound-services). -The data a client can publish is subject to the following limits for different cluster types: +The data a client can publish is subject to the following limits for different +cluster types: | Cluster type | Cluster limits | Maximum entity limits in a cluster | |---|---|---| @@ -73,7 +114,8 @@ The data a client can publish is subject to the following limits for different c ### Step 1: Provide entity data -The SDK has defined different entities to represent each item type. The following entities are supported for the Shopping category: +The SDK has defined different entities to represent each item type. The +following entities are supported for the Shopping category: 1. `ShoppingEntity` 2. `ShoppingCart` @@ -85,7 +127,8 @@ The charts below outline available attributes and requirements for each type. #### `ShoppingEntity` -The `ShoppingEntity` object represents a product, promotion, deal, subscription, or event that developer partners want to publish. +The `ShoppingEntity` object represents a product, promotion, deal, subscription, +or event that developer partners want to publish. ##### `ShoppingEntity` @@ -188,7 +231,10 @@ PNG, JPG, static GIF, WebP ### Step 2: Provide Cluster data -It is recommended to have the content publish job executed in the background (for example, using [WorkManager](https://developer.android.com/topic/libraries/architecture/workmanager)) and scheduled on a regular basis or on an event basis (for example, every time the user opens the app or when the user just added something to their cart). +It is recommended to have the content publish job executed in the background +(for example, using [WorkManager](https://developer.android.com/topic/libraries/architecture/workmanager)) +and scheduled on a regular basis or on an event basis (for example, every time +the user opens the app or when the user just added something to their cart). `AppEngageShoppingClient` is responsible for publishing shopping clusters. @@ -214,11 +260,11 @@ Following APIs are exposed to publish clusters in the client: #### `isServiceAvailable` -This API is used to check if the service is available for integration and whether the content can be presented on the device. +This API is used to check if the service is available for integration and +whether the content can be presented on the device. ##### For Engage SDK v1.6.0 and higher (Recommended) -
## Android skills @@ -236,7 +282,11 @@ If your team uses AI coding tools (such as Gemini in Android Studio), you can au
-You can check the service availability for every cluster type that you intend to publish. The `isServiceAvailable` API accepts a request object, `ServiceAvailabilityRequest`, which contains the cluster types for which service availability needs to be checked. You can find the `ClusterType` enum values required for `ServiceAvailabilityRequest` from the following table. +You can check the service availability for every cluster type that you intend to +publish. The `isServiceAvailable` API accepts a request object, +`ServiceAvailabilityRequest`, which contains the cluster types for which service +availability needs to be checked. You can find the `ClusterType` enum values +required for `ServiceAvailabilityRequest` from the following table. | Cluster Type | Cluster Type Constant | Integer Value | |---|---|---| @@ -298,11 +348,26 @@ You can check the service availability for every cluster type that you intend to ###### Conditional Service Availability Feature -Some integrated apps request a special configuration that enables and disables the Engage service intermittently in order to reduce their serving cost. This intermittent content ingestion strategy, although possible, negatively affects the user and the product -- stale content will not be presented and some surfaces will not be served at all. +Some integrated apps request a special configuration that enables and disables +the Engage service intermittently in order to reduce their serving cost. This +intermittent content ingestion strategy, although possible, negatively affects +the user and the product -- stale content will not be presented and some surfaces +will not be served at all. -Starting with v1.6.0, the Engage SDK allows checking availability for specific cluster types. This provides more flexibility so that if the intermittent content strategy was adopted by a given application, some cluster types can follow that intermittent strategy while other cluster types are always enabled (i.e. continuation clusters). +Starting with v1.6.0, the Engage SDK allows checking availability for specific +cluster types. This provides more flexibility so that if the intermittent +content strategy was adopted by a given application, some cluster types can +follow that intermittent strategy while other cluster types are always enabled +(i.e. continuation clusters). -If the Engage service should not be 'continuously' enabled on all supported devices for whatever reason, and is configured for intermittent ingestion for any set of devices, all continuation cluster publications (e.g. Shopping Cart, Shopping List, Reorder, and Shopping Order Tracking) will be still enabled by default configuration, and the rest of the cluster types will be enabled and disabled intermittently. If intermittent ingestion applies to you but this default configuration is not suitable for your needs, please contact engage-developers@google.com. +If the Engage service should not be 'continuously' enabled on all supported +devices for whatever reason, and is configured for intermittent ingestion for +any set of devices, all continuation cluster publications (e.g. Shopping +Cart, Shopping List, Reorder, and Shopping Order Tracking) will be still +enabled by default configuration, and the rest of the cluster types will be +enabled and disabled intermittently. If intermittent ingestion applies to +you but this default configuration is not suitable for your needs, please +contact engage-developers@google.com. ##### For SDK versions prior to v1.6.0 (Deprecated) @@ -383,12 +448,14 @@ A `RecommendationCluster` object can have the following attributes: .build()) .build()); -When the service receives the request, the following actions take place within one transaction: +When the service receives the request, the following actions take place within +one transaction: - All existing Recommendation Cluster data is removed. - Data from the request is parsed and stored in new Recommendation Clusters. -In case of an error, the entire request is rejected and the existing state is maintained. +In case of an error, the entire request is rejected and the existing state is +maintained. #### `publishFeaturedCluster` @@ -417,16 +484,20 @@ This API is used to publish a `FeaturedCluster` object. .build()) .build()); -When the service receives the request, the following actions take place within one transaction: +When the service receives the request, the following actions take place within +one transaction: - Existing `FeaturedCluster` data from the developer partner is removed. - Data from the request is parsed and stored in the updated Featured Cluster. -In case of an error, the entire request is rejected and the existing state is maintained. +In case of an error, the entire request is rejected and the existing state is +maintained. #### `publishShoppingCarts` -This API is used to publish a list of `ShoppingCart` objects. This is applicable to developer partner publishing separate carts per merchant. Include merchant name in the title when using this API. +This API is used to publish a list of `ShoppingCart` objects. This is +applicable to developer partner publishing separate carts per merchant. Include +merchant name in the title when using this API. > [!IMPORTANT] > **Important:** The publish APIs are upsert APIs; it replaces the existing content. **Don't** call delete and publish APIs subsequently to replace the content as the publish APIs do that inherently. @@ -451,12 +522,14 @@ This API is used to publish a list of `ShoppingCart` objects. This is applicable .build()) .build()) -When the service receives the request, the following actions take place within one transaction: +When the service receives the request, the following actions take place within +one transaction: - Existing `ShoppingCart` data from the developer partner is removed. - Data from the request is parsed and stored in the updated Shopping Cart Cluster. -In case of an error, the entire request is rejected and the existing state is maintained. +In case of an error, the entire request is rejected and the existing state is +maintained. #### `publishShoppingLists` @@ -485,12 +558,14 @@ This API is used to publish a list of `ShoppingList` objects. .build()) .build()); -When the service receives the request, the following actions take place within one transaction: +When the service receives the request, the following actions take place within +one transaction: - Existing `ShoppingList` data from the developer partner is removed. - Data from the request is parsed and stored in the updated Shopping List Cluster. -In case of an error, the entire request is rejected and the existing state is maintained. +In case of an error, the entire request is rejected and the existing state is +maintained. #### `publishShoppingReorderCluster` @@ -519,12 +594,14 @@ This API is used to publish a `ShoppingReorderCluster` object. .build()) .build()); -When the service receives the request, the following actions take place within one transaction: +When the service receives the request, the following actions take place within +one transaction: - Existing `ShoppingReorderCluster` data from the developer partner is removed. - Data from the request is parsed and stored in the updated Reorder Cluster. -In case of an error, the entire request is rejected and the existing state is maintained. +In case of an error, the entire request is rejected and the existing state is +maintained. #### `publishShoppingOrderTrackingCluster` @@ -553,16 +630,20 @@ This API is used to publish a `ShoppingOrderTrackingCluster` object. .build()) .build()); -When the service receives the request, the following actions take place within one transaction: +When the service receives the request, the following actions take place within +one transaction: - Existing `ShoppingOrderTrackingCluster` data from the developer partner is removed. - Data from the request is parsed and stored in the updated Shopping Order Tracking Cluster. -In case of an error, the entire request is rejected and the existing state is maintained. +In case of an error, the entire request is rejected and the existing state is +maintained. #### `publishUserAccountManagementRequest` -This API is used to publish a Sign In card . The signin action directs users to the app's sign in page so that the app can publish content (or provide more personalized content) +This API is used to publish a Sign In card . The signin action directs users to +the app's sign in page so that the app can publish content (or provide more +personalized content) The following metadata is part of the Sign In Card - @@ -615,16 +696,21 @@ The following metadata is part of the Sign In Card - .setSignInCardEntity(SIGN_IN_CARD_ENTITY) .build()); -When the service receives the request, the following actions take place within one transaction: +When the service receives the request, the following actions take place within +one transaction: - Existing `UserAccountManagementCluster` data from the developer partner is removed. - Data from the request is parsed and stored in the updated UserAccountManagementCluster Cluster. -In case of an error, the entire request is rejected and the existing state is maintained. +In case of an error, the entire request is rejected and the existing state is +maintained. #### `updatePublishStatus` -If for any internal business reason, none of the clusters is published, we **strongly recommend** updating the publish status using the **updatePublishStatus** API. This is important because : +If for any internal business reason, none of the clusters is published, +we **strongly recommend** updating the publish status using the +**updatePublishStatus** API. +This is important because : - Providing the status in all scenarios, even when the content is published (STATUS == PUBLISHED), is critical to populate dashboards that use this explicit status to convey the health and other metrics of your integration. - If no content is published but the integration status isn't broken (STATUS == NOT_PUBLISHED), Google can avoid triggering alerts in the app health dashboards. It confirms that content is not published due to an **expected** situation from the provider's standpoint. @@ -664,7 +750,11 @@ The list of eligible publish status codes are : // Reach out to engage-developers@ before using this enum. AppEngagePublishStatusCode.NOT_PUBLISHED_OTHER -If the content is not published due to a user not logged in, Google would recommend publishing the Sign In Card. If for any reason providers are not able to publish the Sign In Card then we recommend calling the **updatePublishStatus** API with the status code **NOT_PUBLISHED_REQUIRES_SIGN_IN** +If the content is not published due to a user not logged in, +Google would recommend publishing the Sign In Card. +If for any reason providers are not able to publish the Sign In Card +then we recommend calling the **updatePublishStatus** API +with the status code **NOT_PUBLISHED_REQUIRES_SIGN_IN** ### Kotlin @@ -695,7 +785,9 @@ This API is used to delete the content of Recommendation Clusters. client.deleteRecommendationClusters(); -When the service receives the request, it removes the existing data from the Recommendation Clusters. In case of an error, the entire request is rejected and the existing state is maintained. +When the service receives the request, it removes the existing data from the +Recommendation Clusters. In case of an error, the entire request is rejected +and the existing state is maintained. > [!NOTE] > **Note:** This api is available from version 1.1.0 onwards. @@ -715,7 +807,9 @@ This API is used to delete the content of Featured Cluster. client.deleteFeaturedCluster(); -When the service receives the request, it removes the existing data from the Featured Cluster. In case of an error, the entire request is rejected and the existing state is maintained. +When the service receives the request, it removes the existing data from the +Featured Cluster. In case of an error, the entire request is rejected +and the existing state is maintained. > [!NOTE] > **Note:** This api is available from version 1.1.0 onwards. @@ -735,7 +829,9 @@ This API is used to delete the content of Shopping Cart Cluster. client.deleteShoppingCartCluster(); -When the service receives the request, it removes the existing data from the Shopping Cart Cluster. In case of an error, the entire request is rejected and the existing state is maintained. +When the service receives the request, it removes the existing data from the +Shopping Cart Cluster. In case of an error, the entire request is rejected +and the existing state is maintained. > [!NOTE] > **Note:** This api is available from version 1.1.0 onwards. @@ -755,7 +851,9 @@ This API is used to delete the content of Shopping List Cluster. client.deleteShoppingListCluster(); -When the service receives the request, it removes the existing data from the Shopping List Cluster. In case of an error, the entire request is rejected and the existing state is maintained. +When the service receives the request, it removes the existing data from the +Shopping List Cluster. In case of an error, the entire request is rejected +and the existing state is maintained. > [!NOTE] > **Note:** This api is available from version 1.1.0 onwards. @@ -775,7 +873,9 @@ This API is used to delete the content of Shopping Reorder Cluster. client.deleteShoppingReorderCluster(); -When the service receives the request, it removes the existing data from the Shopping Reorder Cluster. In case of an error, the entire request is rejected and the existing state is maintained. +When the service receives the request, it removes the existing data from the +Shopping Reorder Cluster. In case of an error, the entire request is rejected +and the existing state is maintained. > [!NOTE] > **Note:** This api is available from version 1.1.0 onwards. @@ -795,7 +895,9 @@ This API is used to delete the content of Shopping Order Tracking Cluster. client.deleteShoppingOrderTrackingCluster(); -When the service receives the request, it removes the existing data from the Shopping Order Tracking Cluster. In case of an error, the entire request is rejected and the existing state is maintained. +When the service receives the request, it removes the existing data from the +Shopping Order Tracking Cluster. In case of an error, the entire request is +rejected and the existing state is maintained. > [!NOTE] > **Note:** This API is available in versions 1.4.0 and higher. @@ -815,7 +917,9 @@ This API is used to delete the content of UserAccountManagement Cluster. client.deleteUserManagementCluster(); -When the service receives the request, it removes the existing data from the UserAccountManagement Cluster. In case of an error, the entire request is rejected and the existing state is maintained. +When the service receives the request, it removes the existing data from the +UserAccountManagement Cluster. In case of an error, the entire request is +rejected and the existing state is maintained. > [!NOTE] > **Note:** This api is available from version 1.1.0 onwards. @@ -845,11 +949,15 @@ This API is used to delete the content of a given cluster type. ... .build()); -When the service receives the request, it removes the existing data from all clusters matching the specified cluster types. Clients can choose to pass one or many cluster types. In case of an error, the entire request is rejected and the existing state is maintained. +When the service receives the request, it removes the existing data from all +clusters matching the specified cluster types. Clients can choose to pass one or +many cluster types. In case of an error, the entire request is rejected and the +existing state is maintained. #### Error handling -It is highly recommended to listen to the task result from the publish APIs such that a follow-up action can be taken to recover and resubmit an successful task. +It is highly recommended to listen to the task result from the publish APIs such +that a follow-up action can be taken to recover and resubmit an successful task. ### Kotlin @@ -893,7 +1001,8 @@ It is highly recommended to listen to the task result from the publish APIs such } }); -The error is returned as an `AppEngageException` with the cause included as an error code. +The error is returned as an `AppEngageException` with the cause included as an +error code. | Error code | Error name | Note | |---|---|---| @@ -907,13 +1016,23 @@ The error is returned as an `AppEngageException` with the cause included as an e ### Step 3: Handle broadcast intents -In addition to making publish content API calls through a job, it is also required to set up a [`BroadcastReceiver`](https://developer.android.com/reference/android/content/BroadcastReceiver) to receive the request for a content publish. +In addition to making publish content API calls through a job, it is also +required to set up a +[`BroadcastReceiver`](https://developer.android.com/reference/android/content/BroadcastReceiver) to receive +the request for a content publish. -The goal of broadcast intents is mainly for app reactivation and forcing data sync. Broadcast intents are not designed to be sent very frequently. It is only triggered when the Engage Service determines the content might be stale (for example, a week old). That way, there is more confidence that the user can have a fresh content experience, even if the application has not been executed for a long period of time. +The goal of broadcast intents is mainly for app reactivation and forcing data +sync. Broadcast intents are not designed to be sent very frequently. It is only +triggered when the Engage Service determines the content might be stale (for +example, a week old). That way, there is more confidence that the user can have +a fresh content experience, even if the application has not been executed for a +long period of time. The `BroadcastReceiver` must be set up in the following two ways: -- Dynamically register an instance of the `BroadcastReceiver` class using `Context.registerReceiver()`. This enables communication from applications that are still live in memory. +- Dynamically register an instance of the `BroadcastReceiver` class using + `Context.registerReceiver()`. This enables communication from applications + that are still live in memory. ### Kotlin @@ -1036,7 +1155,10 @@ The `BroadcastReceiver` must be set up in the following two ways: } -- Statically declare an implementation with the `` tag in your `AndroidManifest.xml` file. This allows the application to receive broadcast intents when it is not running, and also allows the application to publish the content. +- Statically declare an implementation with the `` tag in your + `AndroidManifest.xml` file. This allows the application to receive broadcast + intents when it is not running, and also allows the application to publish + the content. -The following [intents](https://developer.android.com/reference/android/content/Intent) are sent by the service: +The following [intents](https://developer.android.com/reference/android/content/Intent) are sent by the +service: - `com.google.android.engage.action.PUBLISH_RECOMMENDATION` It is recommended to start a `publishRecommendationClusters` call when this intent is received. - `com.google.android.engage.action.PUBLISH_FEATURED` It is recommended to start a `publishFeaturedCluster` call when this intent is received. @@ -1076,15 +1199,20 @@ The following [intents](https://developer.android.com/reference/android/content/ ## Integration workflow -For a step-by-step guide on verifying your integration after it is complete, see [Engage developer integration workflow](https://developer.android.com/guide/playcore/engage/workflow). +For a step-by-step guide on verifying your integration after it is complete, see +[Engage developer integration workflow](https://developer.android.com/guide/playcore/engage/workflow). ## FAQs -See [Engage SDK Frequently Asked Questions](https://developer.android.com/guide/playcore/engage/faq) for FAQs. +See [Engage SDK Frequently Asked Questions](https://developer.android.com/guide/playcore/engage/faq) for +FAQs. ## Contact -Contact [`engage-developers@google.com`](mailto:engage-developers@google.com) if there are any questions during the integration process. Our team replies as soon as possible. +Contact +[`engage-developers@google.com`](mailto:engage-developers@google.com) if there are +any questions during the integration process. Our team replies as soon as +possible. ## Next steps diff --git a/play/engage-sdk-integration/references/android/guide/playcore/engage/social.md b/play/engage-sdk-integration/references/android/guide/playcore/engage/social.md index 32e630b..b8de98f 100644 --- a/play/engage-sdk-integration/references/android/guide/playcore/engage/social.md +++ b/play/engage-sdk-integration/references/android/guide/playcore/engage/social.md @@ -1,6 +1,14 @@ -Boost app engagement by reaching your users where they are. Integrate Engage SDK to deliver personalized recommendations and continuation content directly to users across multiple on-device surfaces, like **[Collections](https://android-developers.googleblog.com/2024/07/introducing-collections-powered-by-engage-sdk.html)** , **[Entertainment Space](https://blog.google/products/android/entertainment-space/)** , and the Play Store. The integration adds less than 50 KB (compressed) to the average APK and takes most apps about a week of developer time. Learn more at our **[business site](http://play.google.com/console/about/programs/EngageSDK)**. +Boost app engagement by reaching your users where they are. Integrate Engage SDK +to deliver personalized recommendations and continuation content directly to +users across multiple on-device surfaces, like +**[Collections](https://android-developers.googleblog.com/2024/07/introducing-collections-powered-by-engage-sdk.html)** , **[Entertainment +Space](https://blog.google/products/android/entertainment-space/)** , and the Play Store. The integration adds +less than 50 KB (compressed) to the average APK and takes most apps about a +week of developer time. Learn more at our **[business +site](http://play.google.com/console/about/programs/EngageSDK)**. -This guide contains instructions for developer partners to deliver social media content to Engage content surfaces. +This guide contains instructions for developer partners to deliver social media +content to Engage content surfaces. ## Integration detail @@ -8,18 +16,22 @@ The following section captures the integration detail. ### Terminology -***Recommendation*** clusters show personalized suggestions from an individual developer partner. +***Recommendation*** clusters show personalized suggestions from an individual +developer partner. Your recommendations take the following structure: -**Recommendation Cluster**: UI view that contains a group of recommendations from the same developer partner. +**Recommendation Cluster**: UI view that contains a group of recommendations +from the same developer partner. -Each Recommendation Cluster consists of one of the following two types of entities : +Each Recommendation Cluster consists of one of the following two types of +entities : - PortraitMediaEntity - SocialPostEntity -**PortraitMediaEntity** must contain 1 portrait image for the post. Profile and Interaction related metadata are optional. +**PortraitMediaEntity** must contain 1 portrait image for the post. Profile and +Interaction related metadata are optional. - Post @@ -62,9 +74,11 @@ Add the `com.google.android.engage:engage-core` library to your app: ### Summary -The design is based on an implementation of a [bound service](https://developer.android.com/guide/components/bound-services). +The design is based on an implementation of a [bound +service](https://developer.android.com/guide/components/bound-services). -The data a client can publish is subject to the following limits for different cluster types: +The data a client can publish is subject to the following limits for different +cluster types: | Cluster type | Cluster limits | Minimum entity limits in a cluster | Maximum entity limits in a cluster | |---|---|---|---| @@ -72,7 +86,8 @@ The data a client can publish is subject to the following limits for different c ### Step 1: Provide entity data -The SDK has defined different entities to represent each item type. The SDK supports the following entities for the Social category: +The SDK has defined different entities to represent each item type. The SDK +supports the following entities for the Social category: 1. `PortraitMediaEntity` 2. `SocialPostEntity` @@ -142,7 +157,8 @@ The charts below outline available attributes and requirements for each type. #### Image specifications -The images are required to be hosted on public CDNs so that Google can access them. +The images are required to be hosted on public CDNs so that Google can access +them. *File formats* @@ -159,7 +175,10 @@ PNG, JPG, static GIF, WebP ### Step 2: Provide Cluster data -It is recommended to have the content publish job executed in the background (for example, using [WorkManager](https://developer.android.com/topic/libraries/architecture/workmanager)) and scheduled on a regular basis or on an event basis (for example, every time the user opens the app or when the user just followed a new account) +It is recommended to have the content publish job executed in the background +(for example, using [WorkManager](https://developer.android.com/topic/libraries/architecture/workmanager)) +and scheduled on a regular basis or on an event basis (for example, every time +the user opens the app or when the user just followed a new account) `AppEngageSocialClient` is responsible for publishing social clusters. @@ -175,11 +194,11 @@ There are following APIs to publish clusters in the client: #### `isServiceAvailable` -This API is used to check if the service is available for integration and whether the content can be presented on the device. +This API is used to check if the service is available for integration and +whether the content can be presented on the device. ##### For Engage SDK v1.6.0 and higher (Recommended) -
## Android skills @@ -197,7 +216,11 @@ If your team uses AI coding tools (such as Gemini in Android Studio), you can au
-You can check the service availability for every cluster type that you intend to publish. The `isServiceAvailable` API accepts a request object, `ServiceAvailabilityRequest`, which contains the cluster types for which service availability needs to be checked. You can find the `ClusterType` enum values required for `ServiceAvailabilityRequest` from the following table. +You can check the service availability for every cluster type that you intend to +publish. The `isServiceAvailable` API accepts a request object, +`ServiceAvailabilityRequest`, which contains the cluster types for which service +availability needs to be checked. You can find the `ClusterType` enum values +required for `ServiceAvailabilityRequest` from the following table. | Cluster Type | Cluster Type Constant | Integer Value | |---|---|---| @@ -255,9 +278,15 @@ You can check the service availability for every cluster type that you intend to ###### Conditional Service Availability Feature -Some integrated apps request a special configuration that enables and disables the Engage service intermittently in order to reduce their serving cost. This intermittent content ingestion strategy, although possible, negatively affects the user and the product -- stale content will not be presented and some surfaces will not be served at all. +Some integrated apps request a special configuration that enables and disables +the Engage service intermittently in order to reduce their serving cost. This +intermittent content ingestion strategy, although possible, negatively affects +the user and the product -- stale content will not be presented and some surfaces +will not be served at all. -Starting with v1.6.0, the Engage SDK allows checking availability for specific cluster types. If you are interested in opting into this feature for any cluster type, please contact engage-developers@google.com. +Starting with v1.6.0, the Engage SDK allows checking availability for specific +cluster types. If you are interested in opting into this feature for any cluster type, +please contact engage-developers@google.com. ##### For SDK versions prior to v1.6.0 (Deprecated) @@ -341,16 +370,20 @@ A `RecommendationCluster` object can have the following attributes: .build()) .build()); -When the service receives the request, the following actions take place within one transaction: +When the service receives the request, the following actions take place within +one transaction: - All existing Recommendation Cluster data is removed. - Data from the request is parsed and stored in new Recommendation Clusters. -In case of an error, the entire request is rejected and the existing state is maintained. +In case of an error, the entire request is rejected and the existing state is +maintained. #### `publishUserAccountManagementRequest` -This API is used to publish a Sign In card . The signin action directs users to the app's sign in page so that the app can publish content (or provide more personalized content) +This API is used to publish a Sign In card . The signin action directs users to +the app's sign in page so that the app can publish content (or provide more +personalized content) The following metadata is part of the Sign In Card - @@ -403,16 +436,21 @@ The following metadata is part of the Sign In Card - .setSignInCardEntity(SIGN_IN_CARD_ENTITY) .build()); -When the service receives the request, the following actions take place within one transaction: +When the service receives the request, the following actions take place within +one transaction: - Existing `UserAccountManagementCluster` data from the developer partner is removed. - Data from the request is parsed and stored in the updated UserAccountManagementCluster Cluster. -In case of an error, the entire request is rejected and the existing state is maintained. +In case of an error, the entire request is rejected and the existing state is +maintained. #### `updatePublishStatus` -If for any internal business reason, none of the clusters is published, we **strongly recommend** updating the publish status using the **updatePublishStatus** API. This is important because : +If for any internal business reason, none of the clusters is published, +we **strongly recommend** updating the publish status using the +**updatePublishStatus** API. +This is important because : - Providing the status in all scenarios, even when the content is published (STATUS == PUBLISHED), is critical to populate dashboards that use this explicit status to convey the health and other metrics of your integration. - If no content is published but the integration status isn't broken (STATUS == NOT_PUBLISHED), Google can avoid triggering alerts in the app health dashboards. It confirms that content is not published due to an **expected** situation from the provider's standpoint. @@ -452,7 +490,11 @@ The list of eligible publish status codes are : // Reach out to engage-developers@ before using this enum. AppEngagePublishStatusCode.NOT_PUBLISHED_OTHER -If the content is not published due to a user not logged in, Google would recommend publishing the Sign In Card. If for any reason providers are not able to publish the Sign In Card then we recommend calling the **updatePublishStatus** API with the status code **NOT_PUBLISHED_REQUIRES_SIGN_IN** +If the content is not published due to a user not logged in, +Google would recommend publishing the Sign In Card. +If for any reason providers are not able to publish the Sign In Card +then we recommend calling the **updatePublishStatus** API +with the status code **NOT_PUBLISHED_REQUIRES_SIGN_IN** ### Kotlin @@ -483,7 +525,9 @@ This API is used to delete the content of Recommendation Clusters. client.deleteRecommendationClusters(); -When the service receives the request, it removes the existing data from the Recommendation Clusters. In case of an error, the entire request is rejected and the existing state is maintained. +When the service receives the request, it removes the existing data from the +Recommendation Clusters. In case of an error, the entire request is rejected +and the existing state is maintained. #### `deleteUserManagementCluster` @@ -500,7 +544,9 @@ This API is used to delete the content of UserAccountManagement Cluster. client.deleteUserManagementCluster(); -When the service receives the request, it removes the existing data from the UserAccountManagement Cluster. In case of an error, the entire request is rejected and the existing state is maintained. +When the service receives the request, it removes the existing data from the +UserAccountManagement Cluster. In case of an error, the entire request is +rejected and the existing state is maintained. #### `deleteClusters` @@ -525,11 +571,15 @@ This API is used to delete the content of a given cluster type. ... .build()); -When the service receives the request, it removes the existing data from all clusters matching the specified cluster types. Clients can choose to pass one or many cluster types. In case of an error, the entire request is rejected and the existing state is maintained. +When the service receives the request, it removes the existing data from all +clusters matching the specified cluster types. Clients can choose to pass one or +many cluster types. In case of an error, the entire request is rejected and the +existing state is maintained. #### Error handling -It is highly recommended to listen to the task result from the publish APIs such that a follow-up action can be taken to recover and resubmit an successful task. +It is highly recommended to listen to the task result from the publish APIs such +that a follow-up action can be taken to recover and resubmit an successful task. client.publishRecommendationClusters( new PublishRecommendationClustersRequest.Builder() @@ -551,7 +601,8 @@ It is highly recommended to listen to the task result from the publish APIs such } }); -The error is returned as an `AppEngageException` with the cause included as an error code. +The error is returned as an `AppEngageException` with the cause included as an +error code. | Error code | Error name | Note | |---|---|---| @@ -565,13 +616,23 @@ The error is returned as an `AppEngageException` with the cause included as an e ### Step 3: Handle broadcast intents -In addition to making publish content API calls through a job, it is also required to set up a [`BroadcastReceiver`](https://developer.android.com/reference/android/content/BroadcastReceiver) to receive the request for a content publish. +In addition to making publish content API calls through a job, it is also +required to set up a +[`BroadcastReceiver`](https://developer.android.com/reference/android/content/BroadcastReceiver) to receive +the request for a content publish. -The goal of broadcast intents is mainly for app reactivation and forcing data sync. Broadcast intents are not designed to be sent very frequently. It is only triggered when the Engage Service determines the content might be stale (for example, a week old). That way, there is more confidence that the user can have a fresh content experience, even if the application has not been executed for a long period of time. +The goal of broadcast intents is mainly for app reactivation and forcing data +sync. Broadcast intents are not designed to be sent very frequently. It is only +triggered when the Engage Service determines the content might be stale (for +example, a week old). That way, there is more confidence that the user can have +a fresh content experience, even if the application has not been executed for a +long period of time. The `BroadcastReceiver` must be set up in the following two ways: -- Dynamically register an instance of the `BroadcastReceiver` class using `Context.registerReceiver()`. This enables communication from applications that are still live in memory. +- Dynamically register an instance of the `BroadcastReceiver` class using + `Context.registerReceiver()`. This enables communication from applications + that are still live in memory. ### Kotlin @@ -609,7 +670,10 @@ The `BroadcastReceiver` must be set up in the following two ways: /*scheduler=*/null); } -- Statically declare an implementation with the `` tag in your `AndroidManifest.xml` file. This allows the application to receive broadcast intents when it is not running, and also allows the application to publish the content. +- Statically declare an implementation with the `` tag in your + `AndroidManifest.xml` file. This allows the application to receive broadcast + intents when it is not running, and also allows the application to publish + the content. -The following [intents](https://developer.android.com/reference/android/content/Intent) will be sent by the service: +The following [intents](https://developer.android.com/reference/android/content/Intent) will be sent by the +service: - `com.google.android.engage.action.PUBLISH_RECOMMENDATION` It is recommended to start a `publishRecommendationClusters` call when receiving this intent. ## Integration workflow -For a step-by-step guide on verifying your integration after it is complete, see [Engage developer integration workflow](https://developer.android.com/guide/playcore/engage/workflow). +For a step-by-step guide on verifying your integration after it is complete, see +[Engage developer integration workflow](https://developer.android.com/guide/playcore/engage/workflow). ## FAQs -See [Engage SDK Frequently Asked Questions](https://developer.android.com/guide/playcore/engage/faq) for FAQs. +See [Engage SDK Frequently Asked Questions](https://developer.android.com/guide/playcore/engage/faq) for +FAQs. ## Contact -Contact [`engage-developers@google.com`](mailto:engage-developers@google.com) if there are any questions during the integration process. Our team will reply as soon as possible. +Contact +[`engage-developers@google.com`](mailto:engage-developers@google.com) if there are +any questions during the integration process. Our team will reply as soon as +possible. ## Next steps diff --git a/play/engage-sdk-integration/references/android/guide/playcore/engage/travel.md b/play/engage-sdk-integration/references/android/guide/playcore/engage/travel.md index aab021e..1ac8584 100644 --- a/play/engage-sdk-integration/references/android/guide/playcore/engage/travel.md +++ b/play/engage-sdk-integration/references/android/guide/playcore/engage/travel.md @@ -1,44 +1,82 @@ -Boost app engagement by reaching your users where they are. Integrate Engage SDK to deliver personalized recommendations and continuation content directly to users across multiple on-device surfaces, like **[Collections](https://android-developers.googleblog.com/2024/07/introducing-collections-powered-by-engage-sdk.html)** , **[Entertainment Space](https://blog.google/products/android/entertainment-space/)** , and the Play Store. The integration adds less than 50 KB (compressed) to the average APK and takes most apps about a week of developer time. Learn more at our **[business site](http://play.google.com/console/about/programs/EngageSDK)**. +Boost app engagement by reaching your users where they are. Integrate Engage SDK +to deliver personalized recommendations and continuation content directly to +users across multiple on-device surfaces, like +**[Collections](https://android-developers.googleblog.com/2024/07/introducing-collections-powered-by-engage-sdk.html)** , **[Entertainment +Space](https://blog.google/products/android/entertainment-space/)** , and the Play Store. The integration adds +less than 50 KB (compressed) to the average APK and takes most apps about a +week of developer time. Learn more at our **[business +site](http://play.google.com/console/about/programs/EngageSDK)**. -This guide contains instructions for developer partners to deliver travel and events content to Engage content surfaces. +This guide contains instructions for developer partners to deliver travel and +events content to Engage content surfaces. ## Integration detail ### Terminology -This integration includes the following cluster types: **Recommendation** , **Featured** , **Reservation** and **Continue Search**. +This integration includes the following cluster types: **Recommendation** , +**Featured** , **Reservation** and **Continue Search**. -- **Recommendation** clusters show personalized travel \& event suggestions from an individual developer partner. These recommendations can be personalized to the user or generalized (for example, trending items). Use these to surface articles, events, lodging, or places of interest recommendations. +- **Recommendation** clusters show personalized travel \& event suggestions + from an individual developer partner. These recommendations can be + personalized to the user or generalized (for example, trending items). Use + these to surface articles, events, lodging, or places of interest + recommendations. - A Recommendation cluster can be made of `ArticleEntity`, `EventEntity`, `LodgingEntity`, `PointOfInterestEntity`, or `StoreEntity` listings, but not a mix of different entity types. Your recommendations take the following structure: - - **Recommendation Cluster:** A UI view that contains a group of recommendations from the same developer partner. + - **Recommendation Cluster:** A UI view that contains a group of + recommendations from the same developer partner. - - **Entity:** An object representing a single item in a cluster. This integration offers some entities that would be surfaced using the Recommendation Cluster: + - **Entity:** An object representing a single item in a cluster. This + integration offers some entities that would be surfaced using the + Recommendation Cluster: - - **ArticleEntity**: ArticleEntity represents a recommendation for text-based content related to travel \& events. It can be used for articles, blogposts, marketing content, news snippets, etc. + - **ArticleEntity**: ArticleEntity represents a recommendation for + text-based content related to travel \& events. It can be used for + articles, blogposts, marketing content, news snippets, etc. ![](https://developer.android.com/static/images/guide/playcore/engage/article-entity-travel.png) **Figure 1:** UI showing a single ArticleEntity within Recommendations cluster. - - **EventEntity**: EventEntity represents an event happening in the future. Event start time is a critical piece of information that needs to be conveyed to the users. + - **EventEntity**: EventEntity represents an event happening in the + future. Event start time is a critical piece of information that + needs to be conveyed to the users. ![](https://developer.android.com/static/images/guide/playcore/engage/event-entity-travel.png) **Figure 2:** UI showing a single EventEntity within Recommendations cluster. - - **LodgingEntity**: LodgingEntity represents an accommodation, such as a hotel, apartment, vacation home for short term and long term rental. + - **LodgingEntity**: LodgingEntity represents an accommodation, such + as a hotel, apartment, vacation home for short term and long term + rental. ![](https://developer.android.com/static/images/guide/playcore/engage/lodging-entity-travel.png) **Figure 3:** UI showing a single LodgingEntity within Recommendations cluster. - - **StoreEntity**: StoreEntity represents a store, restaurant, cafe etc. It highlights content where a dining venue or store is the critical piece of information that needs to be conveyed to the users. + - **StoreEntity**: StoreEntity represents a store, restaurant, cafe + etc. It highlights content where a dining venue or store is the + critical piece of information that needs to be conveyed to the + users. ![](https://developer.android.com/static/images/guide/playcore/engage/store-entity-travel.png) **Figure 4:** UI showing a single StoreEntity within Recommendations cluster. - - **PointOfInterestEntity**: PointOfInterestEntity represents a place of interest like, a gas station, event venue, theme park, museum, tourist attraction, hiking trail etc. It highlights content where location is a critical piece of information that needs to be conveyed to the users. It shouldn't be used for lodging, a store or a dining venue. + - **PointOfInterestEntity**: PointOfInterestEntity represents a + place of interest like, a gas station, event venue, theme park, + museum, tourist attraction, hiking trail etc. It highlights content + where location is a critical piece of information that needs to be + conveyed to the users. It shouldn't be used for lodging, a store or + a dining venue. ![](https://developer.android.com/static/images/guide/playcore/engage/poi-entity-travel.png) **Figure 5:** UI showing a single PointOfInterestEntity within Recommendations cluster. -- The **Reservation** cluster shows content recently engaged by users from multiple developer partners in a single UI grouping. Each developer partner will be allowed to broadcast a maximum of 10 entities in the Reservation cluster. +- The **Reservation** cluster shows content recently engaged by users from + multiple developer partners in a single UI grouping. Each developer partner + will be allowed to broadcast a maximum of 10 entities in the Reservation + cluster. Your reservation content can take the following structure: - - **RestaurantReservationEntity**: RestaurantReservationEntity represents a reservation for a restaurant or cafe and helps users track upcoming or ongoing restaurant reservations. + - **RestaurantReservationEntity**: RestaurantReservationEntity represents + a reservation for a restaurant or cafe and helps users track upcoming or + ongoing restaurant reservations. ![](https://developer.android.com/static/images/guide/playcore/engage/restaurant-reservation-entity-travel.png) **Figure 6.** UI showing a single RestaurantReservationEntity within a Reservation cluster. - - **EventReservationEntity**: EventReservationEntity represents a reservation for an event and helps users track upcoming or ongoing events reservations. Events could include, but not limited to the following: + - **EventReservationEntity**: EventReservationEntity represents a + reservation for an event and helps users track upcoming or ongoing + events reservations. Events could include, but not limited to the + following: - Sports events like reservation for a football match - Gaming events like reservation for eSports @@ -48,21 +86,40 @@ This integration includes the following cluster types: **Recommendation** , **Fe - Education / Training sessions reservations ![](https://developer.android.com/static/images/guide/playcore/engage/event-reservation-entity-travel.png) **Figure 7.** UI showing a single EventReservationEntity within a Reservation cluster. - - **LodgingReservationEntity**: LodgingEntityReservation represents a reservation for travel lodging and helps users track upcoming or ongoing hotel or vacation rental reservations. + - **LodgingReservationEntity**: LodgingEntityReservation represents a + reservation for travel lodging and helps users track upcoming or + ongoing hotel or vacation rental reservations. ![](https://developer.android.com/static/images/guide/playcore/engage/lodging-reservation-entity-travel.png) **Figure 8.** UI showing a single LodgingReservationEntity within a Reservation cluster. - - **TransportationReservationEntity**: TransportationReservationEntity represents reservation for transportation by any mode and helps users track reservations for upcoming or ongoing flight, ferry, train, bus, ride-hailing, or cruise. + - **TransportationReservationEntity**: TransportationReservationEntity + represents reservation for transportation by any mode and helps users + track reservations for upcoming or ongoing flight, ferry, train, bus, + ride-hailing, or cruise. ![](https://developer.android.com/static/images/guide/playcore/engage/transportation-reservation-entity-travel.png) **Figure 9.** UI showing a single TransportationReservationEntity within a Reservation cluster. - - **VehicleRentalReservationEntity**: VehicleRentalReservationEntity represents vehicle rental reservation and helps users track upcoming or ongoing vehicle rental reservations. + - **VehicleRentalReservationEntity**: VehicleRentalReservationEntity + represents vehicle rental reservation and helps users track upcoming + or ongoing vehicle rental reservations. ![](https://developer.android.com/static/images/guide/playcore/engage/vehicle-rental-reservation-entity-travel.png) **Figure 10.** UI showing a single VehicleRentalReservationEntity within a Reservation cluster. -- The **Featured** cluster showcases a selection of entities from multiple developer partners in one UI grouping. There will be a single Featured cluster, which is surfaced near the top of the UI with a priority placement above all Recommendation clusters. Each developer partner will be allowed to broadcast up to 10 entities in the Featured cluster. +- The **Featured** cluster showcases a selection of entities from multiple + developer partners in one UI grouping. There will be a single Featured + cluster, which is surfaced near the top of the UI with a priority placement + above all Recommendation clusters. Each developer partner will be allowed to + broadcast up to 10 entities in the Featured cluster. - - **GenericFeaturedEntity**: GenericFeaturedEntity differs from Recommendation item in that Featured item should be used for a single top content from developers and should represent the single most important content that will be interesting and relevant to users. + - **GenericFeaturedEntity**: GenericFeaturedEntity differs from + Recommendation item in that Featured item should be used for a single + top content from developers and should represent the single most + important content that will be interesting and relevant to users. ![](https://developer.android.com/static/images/guide/playcore/engage/featured-cluster-travel.png) **Figure 11:** UI showing a FeaturedCluster with a list of GenericFeaturedEntity -- The **Continue Search** cluster helps users resume their previous travel search journey by showcasing a list of search queries that the user has recently searched for across all their travel apps. The cluster will be pinned in second position, after reservations and before featured and recommendation clusters. Each developer partner will be allowed to broadcast up to 3 entities in the Continue Search cluster. +- The **Continue Search** cluster helps users resume their previous travel + search journey by showcasing a list of search queries that the user has + recently searched for across all their travel apps. The cluster will be + pinned in second position, after reservations and before featured and + recommendation clusters. Each developer partner will be allowed to broadcast + up to 3 entities in the Continue Search cluster. - **PointOfInterestEntity:** PointOfInterestEntity represents a place of interest like, a gas station, event venue, theme park, museum, tourist attraction, hiking trail etc. It highlights content the user has previously searched for. @@ -79,9 +136,11 @@ Add the `com.google.android.engage:engage-core` library to your app: ### Summary -The design is based on an implementation of a [bound service](https://developer.android.com/guide/components/bound-services). +The design is based on an implementation of a +[bound service](https://developer.android.com/guide/components/bound-services). -The data a client can publish is subject to the following limits for different cluster types: +The data a client can publish is subject to the following limits for different +cluster types: | Cluster type | Cluster limits | Minimum entity limits in a cluster | Maximum entity limits in a cluster | |---|---|---|---| @@ -92,7 +151,8 @@ The data a client can publish is subject to the following limits for different c ### Step 1: Provide entity data -The SDK has defined different entities to represent each item type. We support the following entities for the Travel \& Events category: +The SDK has defined different entities to represent each item type. We support +the following entities for the Travel \& Events category: 1. `GenericFeaturedEntity` 2. `ArticleEntity` @@ -214,7 +274,9 @@ The charts below outline available attributes and requirements for each type. #### `StoreEntity` -The `StoreEntity` object represents an individual store that developer partners want to publish, such as a popular dining place or eatery that are relevant to one's travel experience. +The `StoreEntity` object represents an individual store that developer partners +want to publish, such as a popular dining place or eatery that are relevant to +one's travel experience. | Attribute | Requirement | Description | Format | |---|---|---|---| @@ -452,7 +514,8 @@ Required specifications for image assets are listed in this table: | Landscape (1.91x1) | 600x314 | 1200x628 | | Portrait (4x5) | 480x600 | 960x1200 | -The images are required to be hosted on public CDNs so that Google can access them. +The images are required to be hosted on public CDNs so that Google can access +them. *File formats* @@ -469,13 +532,15 @@ PNG, JPG, static GIF, WebP #### Timezone specifications -Prefer ID (for example, "America/Los_Angeles") over offset (for example, "-07:00"). +Prefer ID (for example, "America/Los_Angeles") over offset (for example, +"-07:00"). Sample usage: `DateTimeZone.forID("America/Los_Angeles")` #### Content Category -The content category allows apps to publish content belonging to multiple categories. This maps the content with some of the predefined categories namely: +The content category allows apps to publish content belonging to multiple +categories. This maps the content with some of the predefined categories namely: - `TYPE_EDUCATION` - `TYPE_SPORTS` @@ -495,25 +560,33 @@ The content category allows apps to publish content belonging to multiple catego - `TYPE_PARENTING` - `TYPE_DATING` -The images are required to be hosted on public CDNs so that Google can access them. +The images are required to be hosted on public CDNs so that Google can access +them. *Guidelines to use the content categories* 1. Some entities like **ArticleEntity** and **GenericFeaturedEntity** are eligible to use any of the content categories. For other entities like **EventEntity** , **EventReservationEntity** , **PointOfInterestEntity**, only a subset of these categories are eligible. Check the list of categories eligible for an entity type before populating the list. -2. Use the specific entity type for some content categories over a combination of the Generic entities and the ContentCategory: +2. Use the specific entity type for some content categories over a combination + of the Generic entities and the ContentCategory: - TYPE_MOVIES_AND_TV_SHOWS - Check out the entities from [Watch integration guide](https://developer.android.com/guide/playcore/engage/watch) before using the generic entities. - TYPE_BOOKS - Check out the [EbookEntity](https://developer.android.com/guide/playcore/engage/read#ebookentity) before using the generic entities. - TYPE_AUDIOBOOKS - Check out [AudiobookEntity](https://developer.android.com/guide/playcore/engage/read#audiobookentity) before using the generic entities. - TYPE_SHOPPING - Check out [ShoppingEntity](https://developer.android.com/guide/playcore/engage/shopping#shoppingEntity) before using the generic entities. - TYPE_FOOD_AND_DRINK - Check out entities from [Food Integration guide](https://developer.android.com/guide/playcore/engage/food) before using the generic entities. -3. The ContentCategory field is optional and should be left blank if the content doesn't belong to any of the categories mentioned earlier. +3. The ContentCategory field is optional and should be left blank if the + content doesn't belong to any of the categories mentioned earlier. -4. In case multiple content categories are provided, provide them in the order of relevance to the content with the most relevant content category placed first in the list. +4. In case multiple content categories are provided, provide them in the order + of relevance to the content with the most relevant content category placed + first in the list. ### Step 2: Provide Cluster data -It is recommended to have the content publish job executed in the background (for example, using [WorkManager](https://developer.android.com/topic/libraries/architecture/workmanager)) and scheduled on a regular basis or on an event basis (for example, every time the user opens the app or when the user just added something to their cart). +It is recommended to have the content publish job executed in the background +(for example, using [WorkManager](https://developer.android.com/topic/libraries/architecture/workmanager)) +and scheduled on a regular basis or on an event basis (for example, every time +the user opens the app or when the user just added something to their cart). `AppEngageTravelClient` is responsible for publishing clusters. @@ -535,11 +608,11 @@ There are following APIs to publish clusters in the client: #### `isServiceAvailable` -This API is used to check if the service is available for integration and whether the content can be presented on the device. +This API is used to check if the service is available for integration and +whether the content can be presented on the device. ##### For Engage SDK v1.6.0 and higher (Recommended) -
## Android skills @@ -557,7 +630,11 @@ If your team uses AI coding tools (such as Gemini in Android Studio), you can au
-You can check the service availability for every cluster type that you intend to publish. The `isServiceAvailable` API accepts a request object, `ServiceAvailabilityRequest`, which contains the cluster types for which service availability needs to be checked. You can find the `ClusterType` enum values required for `ServiceAvailabilityRequest` from the following table. +You can check the service availability for every cluster type that you intend to +publish. The `isServiceAvailable` API accepts a request object, +`ServiceAvailabilityRequest`, which contains the cluster types for which service +availability needs to be checked. You can find the `ClusterType` enum values +required for `ServiceAvailabilityRequest` from the following table. | Cluster Type | Cluster Type Constant | Integer Value | |---|---|---| @@ -617,11 +694,25 @@ You can check the service availability for every cluster type that you intend to ###### Conditional Service Availability Feature -Some integrated apps request a special configuration that enables and disables the Engage service intermittently in order to reduce their serving cost. This intermittent content ingestion strategy, although possible, negatively affects the user and the product -- stale content will not be presented and some surfaces will not be served at all. +Some integrated apps request a special configuration that enables and disables +the Engage service intermittently in order to reduce their serving cost. This +intermittent content ingestion strategy, although possible, negatively affects +the user and the product -- stale content will not be presented and some surfaces +will not be served at all. -Starting with v1.6.0, the Engage SDK allows checking availability for specific cluster types. This provides more flexibility so that if the intermittent content strategy was adopted by a given application, some cluster types can follow that intermittent strategy while other cluster types are always enabled (i.e. continuation clusters). +Starting with v1.6.0, the Engage SDK allows checking availability for specific +cluster types. This provides more flexibility so that if the intermittent +content strategy was adopted by a given application, some cluster types can +follow that intermittent strategy while other cluster types are always enabled +(i.e. continuation clusters). -If the Engage service should not be 'continuously' enabled on all supported devices for whatever reason, and is configured for intermittent ingestion for any set of devices, all continuation cluster publications (e.g. Reservation and Continue Search) will be still enabled by default configuration, and the rest of the cluster types will be enabled and disabled intermittently. If intermittent ingestion applies to you but this default configuration is not suitable for your needs, please contact engage-developers@google.com. +If the Engage service should not be 'continuously' enabled on all supported +devices for whatever reason, and is configured for intermittent ingestion for +any set of devices, all continuation cluster publications (e.g. Reservation +and Continue Search) will be still enabled by default configuration, and the +rest of the cluster types will be enabled and disabled intermittently. If +intermittent ingestion applies to you but this default configuration is not +suitable for your needs, please contact engage-developers@google.com. ##### For SDK versions prior to v1.6.0 (Deprecated) @@ -695,12 +786,14 @@ This API is used to publish a list of `RecommendationCluster` objects. .build()) .build()); -When the service receives the request, the following actions take place within one transaction: +When the service receives the request, the following actions take place within +one transaction: - Existing `RecommendationCluster` data from the developer partner is removed. - Data from the request is parsed and stored in the updated Recommendation Cluster. -In case of an error, the entire request is rejected and the existing state is maintained. +In case of an error, the entire request is rejected and the existing state is +maintained. #### `publishFeaturedCluster` @@ -731,12 +824,14 @@ This API is used to publish a list of `FeaturedCluster` objects. .build()) .build()); -When the service receives the request, the following actions take place within one transaction: +When the service receives the request, the following actions take place within +one transaction: - Existing `FeaturedCluster` data from the developer partner is removed. - Data from the request is parsed and stored in the updated Featured Cluster. -In case of an error, the entire request is rejected and the existing state is maintained. +In case of an error, the entire request is rejected and the existing state is +maintained. #### `publishReservationCluster` @@ -773,12 +868,14 @@ This API is used to publish a `ReservationCluster` object. .build()) .build()); -When the service receives the request, the following actions take place within one transaction: +When the service receives the request, the following actions take place within +one transaction: - Existing `ReservationCluster` data from the developer partner is removed. - Data from the request is parsed and stored in the updated Reservation Cluster. -In case of an error, the entire request is rejected and the existing state is maintained. +In case of an error, the entire request is rejected and the existing state is +maintained. #### `publishContinueSearchCluster` @@ -809,16 +906,20 @@ This API is used to publish a list of `ContinueSearchCluster` objects. .build()) .build()); -When the service receives the request, the following actions take place within one transaction: +When the service receives the request, the following actions take place within +one transaction: - Existing `ContinueSearchCluster` data from the developer partner is removed. - Data from the request is parsed and stored in the updated Continue Search Cluster. -In case of an error, the entire request is rejected and the existing state is maintained. +In case of an error, the entire request is rejected and the existing state is +maintained. #### `publishUserAccountManagementRequest` -This API is used to publish a Sign In card . The signin action directs users to the app's sign in page so that the app can publish content (or provide more personalized content) +This API is used to publish a Sign In card . The signin action directs users to +the app's sign in page so that the app can publish content (or provide more +personalized content) The following metadata is part of the Sign In Card - @@ -871,16 +972,20 @@ The following metadata is part of the Sign In Card - .setSignInCardEntity(SIGN_IN_CARD_ENTITY) .build()); -When the service receives the request, the following actions take place within one transaction: +When the service receives the request, the following actions take place within +one transaction: - Existing `UserAccountManagementCluster` data from the developer partner is removed. - Data from the request is parsed and stored in the updated UserAccountManagementCluster Cluster. -In case of an error, the entire request is rejected and the existing state is maintained. +In case of an error, the entire request is rejected and the existing state is +maintained. #### `updatePublishStatus` -If for any internal business reason, none of the clusters is published, we **strongly recommend** updating the publish status using the **updatePublishStatus** API. This is important because : +If for any internal business reason, none of the clusters is published, we +**strongly recommend** updating the publish status using the +**updatePublishStatus** API. This is important because : - Providing the status in all scenarios, even when the content is published (STATUS == PUBLISHED), is critical to populate dashboards that use this explicit status to convey the health and other metrics of your integration. - If no content is published but the integration status isn't broken (STATUS == NOT_PUBLISHED), Google can avoid triggering alerts in the app health dashboards. It confirms that content is not published due to an **expected** situation from the provider's standpoint. @@ -920,7 +1025,11 @@ The list of eligible publish status codes are : // Reach out to engage-developers@ before using this enum. AppEngagePublishStatusCode.NOT_PUBLISHED_OTHER -If the content is not published due to a user not logged in, Google would recommend publishing the Sign In Card. If for any reason providers are not able to publish the Sign In Card then we recommend calling the **updatePublishStatus** API with the status code **NOT_PUBLISHED_REQUIRES_SIGN_IN** +If the content is not published due to a user not logged in, Google would +recommend publishing the Sign In Card. If for any reason providers are not able +to publish the Sign In Card then we recommend calling the +**updatePublishStatus** API with the status code +**NOT_PUBLISHED_REQUIRES_SIGN_IN** ### Kotlin @@ -951,7 +1060,9 @@ This API is used to delete the content of Recommendation Clusters. client.deleteRecommendationClusters(); -When the service receives the request, it removes the existing data from the Recommendation Clusters. In case of an error, the entire request is rejected and the existing state is maintained. +When the service receives the request, it removes the existing data from the +Recommendation Clusters. In case of an error, the entire request is rejected and +the existing state is maintained. > [!NOTE] > **Note:** This api is available from version 1.1.0 onwards. @@ -971,7 +1082,9 @@ This API is used to delete the content of Featured Cluster. client.deleteFeaturedCluster(); -When the service receives the request, it removes the existing data from the Featured Cluster. In case of an error, the entire request is rejected and the existing state is maintained. +When the service receives the request, it removes the existing data from the +Featured Cluster. In case of an error, the entire request is rejected and the +existing state is maintained. > [!NOTE] > **Note:** This api is available from version 1.1.0 onwards. @@ -991,7 +1104,9 @@ This API is used to delete the content of Reservation Cluster. client.deleteReservationCluster(); -When the service receives the request, it removes the existing data from the Reservation Cluster. In case of an error, the entire request is rejected and the existing state is maintained. +When the service receives the request, it removes the existing data from the +Reservation Cluster. In case of an error, the entire request is rejected and +the existing state is maintained. > [!NOTE] > **Note:** This api is available from version 1.1.0 onwards. @@ -1011,7 +1126,9 @@ This API is used to delete the content of UserAccountManagement Cluster. client.deleteUserManagementCluster(); -When the service receives the request, it removes the existing data from the UserAccountManagement Cluster. In case of an error, the entire request is rejected and the existing state is maintained. +When the service receives the request, it removes the existing data from the +UserAccountManagement Cluster. In case of an error, the entire request is +rejected and the existing state is maintained. > [!NOTE] > **Note:** This api is available from version 1.1.0 onwards. @@ -1031,7 +1148,9 @@ This API is used to delete the content of Continue Search Cluster. client.deleteContinueSearchCluster(); -When the service receives the request, it removes the existing data from the Continue Search Cluster. In case of an error, the entire request is rejected, and the existing state is maintained. +When the service receives the request, it removes the existing data from the +Continue Search Cluster. In case of an error, the entire request is +rejected, and the existing state is maintained. > [!NOTE] > **Note:** This API is available from version 1.5.6 onwards. @@ -1063,11 +1182,15 @@ This API is used to delete the content of a given cluster type. .addClusterType(ClusterType.TYPE_CONTINUE_SEARCH) .build()); -When the service receives the request, it removes the existing data from all clusters matching the specified cluster types. Clients can choose to pass one or many cluster types. In case of an error, the entire request is rejected and the existing state is maintained. +When the service receives the request, it removes the existing data from all +clusters matching the specified cluster types. Clients can choose to pass one or +many cluster types. In case of an error, the entire request is rejected and the +existing state is maintained. #### Error handling -It is highly recommended to listen to the task result from the publish APIs such that a follow-up action can be taken to recover and resubmit an successful task. +It is highly recommended to listen to the task result from the publish APIs such +that a follow-up action can be taken to recover and resubmit an successful task. ### Kotlin @@ -1111,7 +1234,8 @@ It is highly recommended to listen to the task result from the publish APIs such } }); -The error is returned as an `AppEngageException` with the cause included as an error code. +The error is returned as an `AppEngageException` with the cause included as an +error code. | Error code | Error name | Note | |---|---|---| @@ -1125,13 +1249,23 @@ The error is returned as an `AppEngageException` with the cause included as an e ### Step 3: Handle broadcast intents -In addition to making publish content API calls through a job, it is also required to set up a [`BroadcastReceiver`](https://developer.android.com/reference/android/content/BroadcastReceiver) to receive the request for a content publish. +In addition to making publish content API calls through a job, it is also +required to set up a +[`BroadcastReceiver`](https://developer.android.com/reference/android/content/BroadcastReceiver) to receive +the request for a content publish. -The goal of broadcast intents is mainly for app reactivation and forcing data sync. Broadcast intents are not designed to be sent very frequently. It is only triggered when the Engage Service determines the content might be stale (for example, a week old). That way, there is more confidence that the user can have a fresh content experience, even if the application has not been executed for a long period of time. +The goal of broadcast intents is mainly for app reactivation and forcing data +sync. Broadcast intents are not designed to be sent very frequently. It is only +triggered when the Engage Service determines the content might be stale (for +example, a week old). That way, there is more confidence that the user can have +a fresh content experience, even if the application has not been executed for a +long period of time. The `BroadcastReceiver` must be set up in the following two ways: -- Dynamically register an instance of the `BroadcastReceiver` class using `Context.registerReceiver()`. This enables communication from applications that are still live in memory. +- Dynamically register an instance of the `BroadcastReceiver` class using + `Context.registerReceiver()`. This enables communication from applications + that are still live in memory. ### Kotlin @@ -1219,7 +1353,10 @@ The `BroadcastReceiver` must be set up in the following two ways: } -- Statically declare an implementation with the `` tag in your `AndroidManifest.xml` file. This allows the application to receive broadcast intents when it is not running, and also allows the application to publish the content. +- Statically declare an implementation with the `` tag in your + `AndroidManifest.xml` file. This allows the application to receive broadcast + intents when it is not running, and also allows the application to publish + the content. -The following [intents](https://developer.android.com/reference/android/content/Intent) is sent by the service: +The following [intents](https://developer.android.com/reference/android/content/Intent) is sent by the +service: - `com.google.android.engage.action.PUBLISH_RECOMMENDATION` It is recommended to start a `publishRecommendationClusters` call when receiving this intent. - `com.google.android.engage.action.PUBLISH_FEATURED` It is recommended to start a `publishFeaturedCluster` call when receiving this intent. @@ -1251,15 +1389,19 @@ The following [intents](https://developer.android.com/reference/android/content/ ## Integration workflow -For a step-by-step guide on verifying your integration after it is complete, see [Engage developer integration workflow](https://developer.android.com/guide/playcore/engage/workflow). +For a step-by-step guide on verifying your integration after it is complete, see +[Engage developer integration workflow](https://developer.android.com/guide/playcore/engage/workflow). ## FAQs -See [Engage SDK Frequently Asked Questions](https://developer.android.com/guide/playcore/engage/faq) for FAQs. +See [Engage SDK Frequently Asked Questions](https://developer.android.com/guide/playcore/engage/faq) for +FAQs. ## Contact -Contact [`engage-developers@google.com`](mailto:engage-developers@google.com) if there are any questions during the integration process. +Contact +[`engage-developers@google.com`](mailto:engage-developers@google.com) if there are +any questions during the integration process. ## Next steps diff --git a/play/engage-sdk-integration/references/android/guide/playcore/engage/tv/continue-watching/index.md b/play/engage-sdk-integration/references/android/guide/playcore/engage/tv/continue-watching/index.md index dd88e0e..555b293 100644 --- a/play/engage-sdk-integration/references/android/guide/playcore/engage/tv/continue-watching/index.md +++ b/play/engage-sdk-integration/references/android/guide/playcore/engage/tv/continue-watching/index.md @@ -1,10 +1,19 @@ -Continue watching leverages the **Continuation cluster** to show unfinished videos, and next episodes to be watched from the same TV show, from multiple apps in one UI grouping. You can feature your entities in this continuation cluster. Follow this guide to learn how to enhance user engagement through the continue watching experience using [Engage SDK](https://developer.android.com/guide/playcore/engage). +Continue watching leverages the **Continuation cluster** to show unfinished +videos, and next episodes to be watched from the same TV show, from multiple +apps in one UI grouping. You can feature your entities in this continuation +cluster. Follow this guide to learn how to enhance user engagement through the +continue watching experience using [Engage SDK](https://developer.android.com/guide/playcore/engage). -You manage the continuation cluster by using the client API in a TV app or from a REST API: +You manage the continuation cluster by using the client API in a TV app or from +a REST API: - [Integrate continue watching on Android TV](https://developer.android.com/guide/playcore/engage/tv/continue-watching/client) - [Integrate continue watching using REST API](https://developer.android.com/guide/playcore/engage/tv/continue-watching/rest) ## Sample code -This [sample app](https://github.com/googlesamples/tv-video-discovery-samples) demonstrates how you can integrate with Engage SDK to send personalized user data to Google. It shows how to build a common module that you can import in both mobile and TV apps. The sample also illustrates when to call the publish and delete APIs, as well as how to use Workers to make these calls. \ No newline at end of file +This [sample app](https://github.com/googlesamples/tv-video-discovery-samples) demonstrates how you can integrate with Engage SDK +to send personalized user data to Google. It shows how to build a common module +that you can import in both mobile and TV apps. The sample also illustrates +when to call the publish and delete APIs, as well as how to use Workers to +make these calls. \ No newline at end of file diff --git a/play/engage-sdk-integration/references/android/guide/playcore/engage/tv/entitlements.md b/play/engage-sdk-integration/references/android/guide/playcore/engage/tv/entitlements.md index 5ef572e..a8f8c30 100644 --- a/play/engage-sdk-integration/references/android/guide/playcore/engage/tv/entitlements.md +++ b/play/engage-sdk-integration/references/android/guide/playcore/engage/tv/entitlements.md @@ -1,11 +1,17 @@ -This guide contains instructions for developers to share app subscription and entitlement data with Google TV using [Engage SDK](https://developer.android.com/guide/playcore/engage). Users can find content they are entitled to and enable Google TV to deliver highly relevant content recommendations to users, directly within Google TV experiences on TV, mobile, and tablet. +This guide contains instructions for developers to share app subscription and +entitlement data with Google TV using [Engage SDK](https://developer.android.com/guide/playcore/engage). Users can find +content they are entitled to and enable Google TV to deliver highly relevant +content recommendations to users, directly within Google TV experiences on TV, +mobile, and tablet. ## Prerequisites > [!IMPORTANT] > **Important:** [Express interest in developing with Engage](http://g.co/tv/vda). -Onboarding the media actions feed is required before you can use the device entitlement API. If you haven't already done so, complete the [media actions feed](https://developers.google.com/actions/media?authuser=0) onboarding process. +Onboarding the media actions feed is required before you can use the device +entitlement API. If you haven't already done so, complete the [media actions +feed](https://developers.google.com/actions/media?authuser=0) onboarding process. ## Pre-work @@ -20,13 +26,17 @@ Complete the [Pre-work](https://developer.android.com/guide/playcore/engage/tv/g ## Integration -This section provides the necessary code examples and instructions for implementing `SubscriptionEntity` to manage various subscription types. +This section provides the necessary code examples and instructions for +implementing `SubscriptionEntity` to manage various subscription types. ### Common tier subscription -For users with basic subscriptions to media provider services, for example, a service that has one subscription tier that grants access to all the paid content, provide these essential details: +For users with basic subscriptions to media provider services, for example, a +service that has one subscription tier that grants access to all the paid +content, provide these essential details: -1. `SubscriptionType`: Clearly indicate the specific subscription plan the user has. +1. `SubscriptionType`: Clearly indicate the specific subscription plan the user + has. - `SUBSCRIPTION_TYPE_ACTIVE`: User has an active paid subscription. - `SUBSCRIPTION_TYPE_ACTIVE_TRIAL`: User has a trial subscription. @@ -35,9 +45,11 @@ For users with basic subscriptions to media provider services, for example, a se > [!IMPORTANT] > **Important:** Only users with `SUBSCRIPTION_TYPE_ACTIVE` or `SUBSCRIPTION_TYPE_ACTIVE_TRIAL` are eligible for personalized content recommendations based on their subscription. -2. `ExpirationTimeMillis`: Optional time in milliseconds. Specify when the subscription is set to expire. +2. `ExpirationTimeMillis`: Optional time in milliseconds. Specify when the + subscription is set to expire. -3. `ProviderPackageName`: Specify the package name of the app that handles the subscription. +3. `ProviderPackageName`: Specify the package name of the app that handles the + subscription. Example for the sample media provider feed. @@ -71,7 +83,9 @@ The following example creates a `SubscriptionEntity` for a user: ### Premium subscription -If app offer multi-tiered premium subscription packages, which includes expanded content or features beyond the common tier, represent this by adding one or more entitlements to Subscription. +If app offer multi-tiered premium subscription packages, which includes expanded +content or features beyond the common tier, represent this by adding one or more +entitlements to Subscription. This entitlement has the following fields: @@ -144,7 +158,9 @@ The following example creates a `SubscriptionEntity` for a subscribed user: ### Subscription for linked service package -While subscriptions typically belong to the originating app's media provider, a subscription can be attributed to a linked service package by specifying the linked service package name within the subscription. +While subscriptions typically belong to the originating app's media provider, a +subscription can be attributed to a linked service package by specifying the +linked service package name within the subscription. Following code sample demonstrate how to create user subscription. @@ -159,7 +175,8 @@ Following code sample demonstrate how to create user subscription. .setExpirationTimeMillis(1767052800000) .build() -In addition, if the user has another subscription to a subsidiary service, add another subscription and set the linked service package name accordingly. +In addition, if the user has another subscription to a subsidiary service, add +another subscription and set the linked service package name accordingly. // Subscription for linked service package val linkedSubscription = Subscription.Builder() @@ -194,9 +211,11 @@ Optionally, add entitlements to a linked service subscription too. Run the content publish job while the app is in the foreground. -Use the `publishSubscriptionCluster()` method, from the `AppEngagePublishClient` class, to publish a `SubscriptionCluster` object. +Use the `publishSubscriptionCluster()` method, from the +`AppEngagePublishClient` class, to publish a `SubscriptionCluster` object. -Make sure to initialize the client and check for service availability as described in the [Getting Started guide](https://developer.android.com/guide/playcore/engage/tv/getting-started#common-integration). +Make sure to initialize the client and check for service availability as +described in the [Getting Started guide](https://developer.android.com/guide/playcore/engage/tv/getting-started#common-integration). client.publishSubscription( PublishSubscriptionRequest.Builder() @@ -205,22 +224,35 @@ Make sure to initialize the client and check for service availability as describ .build() ) -Use `setSubscription()` to verify that user should have only one subscription to the service. +Use `setSubscription()` to verify that user should have only one subscription to +the service. -Use `addLinkedSubscription()`, or `addLinkedSubscriptions()` which accept a list of linked subscriptions, to enable user to have zero or more linked subscriptions. +Use `addLinkedSubscription()`, or `addLinkedSubscriptions()` which accept a list +of linked subscriptions, to enable user to have zero or more linked +subscriptions. -When the service receives the request, a new entry is created and the old entry is automatically deleted after 60 days. The system always uses the latest entry. In case of an error, the entire request is rejected and the existing state is maintained. +When the service receives the request, a new entry is created and the old entry +is automatically deleted after 60 days. The system always uses the latest entry. +In case of an error, the entire request is rejected and the existing state is +maintained. ### Keep subscription up-to-date -1. To provide immediate updates upon changes, call `publishSubscriptionCluster` whenever a user's subscription state changes like activation, deactivation, upgrades, downgrades. +1. To provide immediate updates upon changes, call + `publishSubscriptionCluster` whenever a user's subscription state changes + like activation, deactivation, upgrades, downgrades. -2. To provide regular validation for ongoing accuracy, call `publishSubscriptionCluster` at least once per month. +2. To provide regular validation for ongoing accuracy, call + `publishSubscriptionCluster` at least once per month. > [!NOTE] > **Note:** As Google TV automatically deletes historical data beyond 60 days to safeguard user privacy, publishing user subscription data at least once per month verify the validity of data. Unlike `publishContinuationCluster` for continue watching data, don't set `syncAcrossDevices` flag, as subscription information is by default used to provide content across all devices. -3. To delete the Engage data, manually delete a user's data from the Google TV server before the standard 60-day retention period, use the `client.deleteClusters` method. This deletes all existing Engage data for the account profile, or for the entire account depending on the given [`DeleteReason`](https://developer.android.com/reference/com/google/android/engage/service/DeleteReason). +3. To delete the Engage data, manually delete a user's data from the + Google TV server before the standard 60-day retention period, use the + `client.deleteClusters` method. This deletes all existing Engage + data for the account profile, or for the entire account depending on the + given [`DeleteReason`](https://developer.android.com/reference/com/google/android/engage/service/DeleteReason). The following code snippet shows how to remove a user subscription: @@ -234,7 +266,8 @@ When the service receives the request, a new entry is created and the old entry .build() ) - The following code snippet demonstrates removal of user subscription when user revokes the consent: + The following code snippet demonstrates removal of user subscription + when user revokes the consent: // If the user revokes the consent to share across device, make the call // to remove subscription and other Engage data from all google @@ -246,7 +279,8 @@ When the service receives the request, a new entry is created and the old entry .build() ) - Following code demonstrates how to remove subscription data on user profile deletion. + Following code demonstrates how to remove subscription data on user profile + deletion. // If the user delete a specific profile, you must make the following call // to remove subscription data and other Engage data. @@ -259,11 +293,13 @@ When the service receives the request, a new entry is created and the old entry ### Testing -This section provides a step-by-step guide for testing subscription implementation. Verify data accuracy and proper functionality before launch. +This section provides a step-by-step guide for testing subscription +implementation. Verify data accuracy and proper functionality before launch. #### Publish Integration checklist -1. Publishing should happen when the app is in the foreground and user the actively interacting with it. +1. Publishing should happen when the app is in the foreground and user the + actively interacting with it. 2. Publish when: @@ -272,9 +308,12 @@ This section provides a step-by-step guide for testing subscription implementati - User purchases new subscription. - User upgrades subscription. - User subscription expires. -3. Check if app is correctly calling `isServiceAvailable()` and `publishClusters()` APIs in logcat, on the publishing events. +3. Check if app is correctly calling `isServiceAvailable` and + `publishClusters` APIs in logcat, on the publishing events. -4. Verify that data is visible in the verification app. The verification app should display subscription as a separate row. When the publish API is invoked, the data should show up in the verification app. +4. Verify that data is visible in the verification app. The verification app + should display subscription as a separate row. When the publish API is + invoked, the data should show up in the verification app. > [!IMPORTANT] > **Important:** Verify that the [Engage Service Flag](https://developer.android.com/guide/playcore/engage/workflow#switch-to-prod) is **not** set to production. @@ -292,15 +331,23 @@ This section provides a step-by-step guide for testing subscription implementati To test your integration, use the [verification app](https://developer.android.com/guide/playcore/engage/tv/getting-started#testing). 1. For each of the events, check if app has invoked the `publishSubscription` API. Verify the published data in the verification app. **Verify that everything is green in verification app** -2. If all the entity's information is correct, it shows an "All Good" green check in all entities. +2. If all the entity's information is correct, it shows an "All Good" green + check in all entities. ![Verification App Success Screenshot](https://developer.android.com/static/images/guide/playcore/engage/ett-va-success.png) **Figure 1.** Successful subscription 3. Problems are also highlighted in verification app ![Verification App Error Screenshot](https://developer.android.com/static/images/guide/playcore/engage/ett-va-error.png) **Figure 2.**Subscription unsuccessful -4. To see the problems in the bundled subscription, use the TV remote to focus on that specific bundled subscription and click to see the problems. You might have to first focus on the row and move to the right to find Bundled Subscription card. The problems are highlighted as red as shown in Fig 3. Also, use the remote to move down to see problems in the entitlements within bundled subscription +4. To see the problems in the bundled subscription, use the TV remote to focus + on that specific bundled subscription and click to see the problems. You + might have to first focus on the row and move to the right to find Bundled + Subscription card. The problems are highlighted as red as shown in Fig 3. + Also, use the remote to move down to see problems in the entitlements within + bundled subscription ![Verification App Error Details Screenshot](https://developer.android.com/static/images/guide/playcore/engage/ett-va-error-details.png) **Figure 3.**Subscription Errors -5. To see the problems in the entitlement, use the TV remote to focus on that specific entitlement and click to see the problems. The problems are highlighted as red. +5. To see the problems in the entitlement, use the TV remote to focus on that + specific entitlement and click to see the problems. The problems are + highlighted as red. ![Verification App Error Screenshot](https://developer.android.com/static/images/guide/playcore/engage/ett-va-details.png) **Figure 4.**Subscription Error Details \ No newline at end of file diff --git a/play/engage-sdk-integration/references/android/guide/playcore/engage/tv/getting-started.md b/play/engage-sdk-integration/references/android/guide/playcore/engage/tv/getting-started.md index 578edc4..6936360 100644 --- a/play/engage-sdk-integration/references/android/guide/playcore/engage/tv/getting-started.md +++ b/play/engage-sdk-integration/references/android/guide/playcore/engage/tv/getting-started.md @@ -1,8 +1,11 @@
-[Engage SDK](https://developer.android.com/guide/playcore/engage) lets you deliver personalized recommendations and continuation content directly to users on Google TV. +[Engage SDK](https://developer.android.com/guide/playcore/engage) lets you deliver personalized recommendations +and continuation content directly to users on Google TV. -This guide covers how to get started with Engage SDK integrations for TV. After you complete the pre-work on this page, you can integrate one or more of the TV features: +This guide covers how to get started with Engage SDK integrations for TV. After +you complete the pre-work on this page, you can integrate one or more +of the TV features: - [Publish continue watching data](https://developer.android.com/guide/playcore/engage/tv/continue-watching) - [Publish device entitlements](https://developer.android.com/guide/playcore/engage/tv/entitlements) @@ -12,13 +15,16 @@ This guide covers how to get started with Engage SDK integrations for TV. After Before you begin, complete the following steps: -1. [Express interest in developing with Engage SDK](http://g.co/tv/engage) to enroll in the program, if eligible. +1. [Express interest in developing with Engage SDK](http://g.co/tv/engage) to enroll in + the program, if eligible. -2. Verify that your app targets Android 4.4 (API level 19) or higher for this integration. +2. Verify that your app targets Android 4.4 (API level 19) or higher for this + integration. 3. Add the `com.google.android.engage` library to your app: - There are separate SDKs to use in the integration: one for mobile apps and one for TV apps. + There are separate SDKs to use in the integration: one for mobile apps and + one for TV apps. ### Mobile @@ -29,18 +35,23 @@ Before you begin, complete the following steps: ### TV dependencies { - implementation 'com.google.android.engage:engage-tv:1.0.6' + implementation 'com.google.android.engage:engage-tv:1.1.0' } 4. Add permission for `WRITE_EPG_DATA` for TV APK -5. Verify reliable content publishing by using a background service, such as `androidx.work`, for scheduling. +5. Verify reliable content publishing by using a background service, such as + `androidx.work`, for scheduling. -6. Test your implementation using the verification app as outlined in the [Testing section](https://developer.android.com/guide/playcore/engage/tv/getting-started#testing). +6. Test your implementation using the verification app as outlined in the + [Testing section](https://developer.android.com/guide/playcore/engage/tv/getting-started#testing). -7. In your production app, set the Engage service environment to production by adding the `` element directly under the `` tag in your `AndroidManifest.xml` file. Don't place this tag inside an ``. +7. In your production app, set the Engage service environment to production by + adding the `` element directly under the `` tag + in your `AndroidManifest.xml` file. + Don't place this tag inside an ``. @@ -57,13 +68,37 @@ Before you begin, complete the following steps: ### Initialize the client -Use `AppEngagePublishClient` to interact with the service. Always check if the service is available before publishing. +Use `AppEngagePublishClient` to interact with the service. Always check if the +service is available before publishing. + +You can check the service availability for every cluster type that you intend to +publish. The `isServiceAvailable` API accepts a request object, +`ServiceAvailabilityRequest`, which contains the cluster types for which service +availability needs to be checked. You can find the `ClusterType` enum values +required for `ServiceAvailabilityRequest` from the following table. + +| Cluster Type | Cluster Type Constant | Integer Value | +|---|---|---| +| Unknown | `TYPE_UNKNOWN` | 0 | +| Recommendation Cluster | `TYPE_RECOMMENDATION` | 1 | +| Continuation Cluster | `TYPE_CONTINUATION` | 3 | val client = AppEngagePublishClient(context) - client.isServiceAvailable().addOnCompleteListener { task -> - if (task.isSuccessful && task.result) { - // Service is available, proceed with publishing + val request = ServiceAvailabilityRequest.Builder() + .addIntendedClusterType(ClusterType.TYPE_CONTINUATION) + .addIntendedClusterType(ClusterType.TYPE_RECOMMENDATION) + .build() + + client.isServiceAvailable(request).addOnCompleteListener { task -> + if (task.isSuccessful) { + val availabilityMap = task.result + if (availabilityMap[ClusterType.TYPE_CONTINUATION] == true) { + // Proceed with publishing continuation content + } + if (availabilityMap[ClusterType.TYPE_RECOMMENDATION] == true) { + // Proceed with publishing recommendation content + } } else { // Service is not available or call failed } @@ -71,7 +106,8 @@ Use `AppEngagePublishClient` to interact with the service. Always check if the s ### Create an account profile -`AccountProfile` identifies the user. You can specify an account ID, and optionally a profile ID and locale. +`AccountProfile` identifies the user. You can specify an account ID, and +optionally a profile ID and locale. val accountProfile = AccountProfile.Builder() .setAccountId("your_users_account_id") @@ -79,15 +115,19 @@ Use `AppEngagePublishClient` to interact with the service. Always check if the s .setLocale(Locale.US.toLanguageTag()) // Optional, e.g., "en-US" .build() -An `AccountProfile` must be provided with an account ID in order for content to be synchronized between devices. See [Cross-device syncing](https://developer.android.com/guide/playcore/engage/tv/continue-watching/client#cross-device_syncing). +An `AccountProfile` must be provided with an account ID in order for content to +be synchronized between devices. See [Cross-device syncing](https://developer.android.com/guide/playcore/engage/tv/continue-watching/client#cross-device_syncing). ## Testing To test your integration, download the verification app: -[Download verification app](https://developer.android.com/guide/playcore/engage/tv/getting-started#lightbox-trigger) +[Download verification +app](https://developer.android.com/guide/playcore/engage/tv/getting-started#lightbox-trigger) -The verification app is an Android app with capabilities to help you test your integration. It let you to check data accuracy and proper functionality by verifying published data and broadcast intents before launch. +The verification app is an Android app with capabilities to help you test your +integration. It let you to check data accuracy and proper functionality by +verifying published data and broadcast intents before launch. 1. Install and open the Engage Verification app. 2. If the value of `isServiceAvailable` is `false` in the verification app, click the **Toggle** button within the verification app to set it to `true`. diff --git a/play/engage-sdk-integration/references/android/guide/playcore/engage/tv/recommendations.md b/play/engage-sdk-integration/references/android/guide/playcore/engage/tv/recommendations.md index 404104f..a2fd19b 100644 --- a/play/engage-sdk-integration/references/android/guide/playcore/engage/tv/recommendations.md +++ b/play/engage-sdk-integration/references/android/guide/playcore/engage/tv/recommendations.md @@ -1,6 +1,11 @@ -This guide contains instructions for developers to integrate their recommended video content, using the [Engage SDK](https://developer.android.com/guide/playcore/engage), to populate recommendations experiences across Google surfaces, such as TV, mobile, and tablet. +This guide contains instructions for developers to integrate their recommended +video content, using the [Engage SDK](https://developer.android.com/guide/playcore/engage), to populate recommendations +experiences across Google surfaces, such as TV, mobile, and tablet. -Recommendation leverages the **Recommendation cluster** to show movies and TV shows, from multiple apps in one UI grouping. Each developer partner can broadcast a maximum **of 25 entities** in each recommendations cluster and there can be a maximum **of 7** recommendation clusters per request. +Recommendation leverages the **Recommendation cluster** to show movies and TV +shows, from multiple apps in one UI grouping. Each developer partner can +broadcast a maximum **of 25 entities** in each recommendations cluster and there +can be a maximum **of 7** recommendation clusters per request. ## Pre-work @@ -16,17 +21,22 @@ Complete the [Pre-work](https://developer.android.com/guide/playcore/engage/tv/g ## Integration -`AppEngagePublishClient` publishes the recommendation cluster. Use the `publishRecommendationClusters` method to publish a recommendations object. +`AppEngagePublishClient` publishes the recommendation cluster. Use the +`publishRecommendationClusters` method to publish a recommendations object. -Make sure to initialize the client and check for service availability as described in the [Getting Started guide](https://developer.android.com/guide/playcore/engage/tv/getting-started#common-integration). +Make sure to initialize the client and check for service availability as +described in the [Getting Started guide](https://developer.android.com/guide/playcore/engage/tv/getting-started#common-integration). client.publishRecommendationClusters(recommendationRequest) ### Upserting recommendation clusters -Clusters are logical grouping of the entities. The following code examples explains how to build the clusters based on your preference and how to create a publishing request and upsert all clusters. +Clusters are logical grouping of the entities. The following code examples +explains how to build the clusters based on your preference and how to create a +publishing request and upsert all clusters. -The [`RecommendationClusterType`](https://developer.android.com/reference/com/google/android/engage/common/datamodel/RecommendationClusterType) determines how the cluster will be displayed. +The [`RecommendationClusterType`](https://developer.android.com/reference/com/google/android/engage/common/datamodel/RecommendationClusterType) determines how the +cluster will be displayed. // cluster for popular movies val recommendationCluster1 = RecommendationCluster @@ -63,7 +73,8 @@ The [`RecommendationClusterType`](https://developer.android.com/reference/com/go .addRecommendationCluster(recommendationCluster2) .build() -When the service receives the request, the following actions occur within one transaction: +When the service receives the request, the following actions occur within one +transaction: - Existing `RecommendationsCluster` data from the developer partner is removed. - Data from the request is parsed and stored in the updated `RecommendationsCluster`. In case of an error, the entire request is rejected and the existing state is maintained. @@ -73,17 +84,27 @@ When the service receives the request, the following actions occur within one tr ### Cross-device sync -`SyncAcrossDevices` flag controls whether a user's recommendations cluster data is shared with Google TV and available across their devices such as TV, phone, tablets. In order for the recommendation to work, it must be set to true. +`SyncAcrossDevices` flag controls whether a user's recommendations cluster data +is shared with Google TV and available across their devices such as TV, phone, +tablets. In order for the recommendation to work, it must be set to true. ### Obtain consent -The media application must provide a clear setting to enable or disable cross-device syncing. Explain the benefits to the user and store the user's preference once and apply it in `publishRecommendations` Request accordingly. To get the most out of cross-device feature, verify app obtains user consent and enables `SyncAcrossDevices` to `true`. +The media application must provide a clear setting to enable or disable +cross-device syncing. Explain the benefits to the user and store the user's +preference once and apply it in `publishRecommendations` Request accordingly. To +get the most out of cross-device feature, verify app obtains user +consent and enables `SyncAcrossDevices` to `true`. ### Delete the Engage data -To manually delete a user's data from the Google TV server before the standard 60-day retention period, use the `client.deleteClusters()` method. Upon receiving the request, the service deletes all existing Engage data for the account profile, or for the entire account. +To manually delete a user's data from the Google TV server before the standard +60-day retention period, use the `client.deleteClusters()` method. Upon +receiving the request, the service deletes all existing Engage +data for the account profile, or for the entire account. -The [`DeleteReason`](https://developer.android.com/reference/com/google/android/engage/service/DeleteReason) enum defines the reason for data deletion. The following code removes recommendations on logout. +The [`DeleteReason`](https://developer.android.com/reference/com/google/android/engage/service/DeleteReason) enum defines the reason for data deletion. +The following code removes recommendations on logout. // If the user logs out from your media app, you must make the following call // to remove recommendations data from the current Google TV device, otherwise, @@ -108,7 +129,8 @@ The [`DeleteReason`](https://developer.android.com/reference/com/google/android/ ## Create entities -The SDK has defined different entities to represent each item type. Following entities are supported for the Recommendation cluster: +The SDK has defined different entities to represent each item type. Following +entities are supported for the Recommendation cluster: 1. [`MediaActionFeedEntity`](https://developer.android.com/reference/com/google/android/engage/video/datamodel/MediaActionFeedEntity) 2. [`MovieEntity`](https://developer.android.com/reference/com/google/android/engage/video/datamodel/MovieEntity) @@ -118,21 +140,27 @@ The SDK has defined different entities to represent each item type. Following en ### Provide descriptions -Provide a short description for each entity; this description will be displayed when users hover over the entity, providing them with additional details. +Provide a short description for each entity; this description will be displayed +when users hover over the entity, providing them with additional details. ### Call to action text -Provide an optional call to action text for each entity. This text will be displayed to the user to encourage engagement. +Provide an optional call to action text for each entity. This text will be +displayed to the user to encourage engagement. ### Tags -Optionally provide a list of tags for each entity. Tags can be used for categorization and filtering. +Optionally provide a list of tags for each entity. Tags can be used for +categorization and filtering. ### Platform specific playBack URIs -Create playback URIs for each supported platform: Android TV, Android, or iOS. This allows the system to select the appropriate URI for video playback on the respective platform. +Create playback URIs for each supported platform: Android TV, Android, or iOS. +This allows the system to select the appropriate URI for video playback on the +respective platform. -In the rare case when the playback URIs are identical for all platforms, repeat it for every platform. +In the rare case when the playback URIs are identical for all platforms, +repeat it for every platform. // Required. Set this when you want recommended entities to show up on // Google TV @@ -170,7 +198,12 @@ In the rare case when the playback URIs are identical for all platforms, repeat ### Poster images -Poster images require a URI and pixel dimensions (height and width). Target different form factors by providing multiple poster images, but verify all images maintain a 16:9 aspect ratio and a minimum height of 200 pixels for correct display of the "Recommendations" entity, especially within Google's [Entertainment Space](https://support.google.com/entertainmentspace/answer/10346911). Images with a height less than 200 pixels may not be shown. +Poster images require a URI and pixel dimensions (height and width). Target +different form factors by providing multiple poster images, but verify all +images maintain a 16:9 aspect ratio and a minimum height of 200 pixels for +correct display of the "Recommendations" entity, especially within Google's +[Entertainment Space](https://support.google.com/entertainmentspace/answer/10346911). Images with a height less than 200 +pixels may not be shown. Image image1 = new Image.Builder() .setImageUri(Uri.parse("http://www.example.com/entity_image1.png");) @@ -189,7 +222,9 @@ Poster images require a URI and pixel dimensions (height and width). Target diff ### Recommendation reason -Optionally provide a recommendation reason which can be used by Google TV to construct reasons as to why to suggest a specific Movie or TV Show to the user. +Optionally provide a recommendation reason which can be used by Google +TV to construct reasons as to why to suggest a specific Movie or TV Show to +the user. //Allows us to construct reason: "Because it is top 10 on your Channel" val topOnPartner = RecommendationReasonTopOnPartner @@ -236,7 +271,11 @@ Optionally provide a recommendation reason which can be used by Google TV to con ### Display time window -If an entity should only be available for a limited time, set a custom expiration time. Without an explicit expiration time, entities will automatically expire and be erased after 60 days. So set an expiration time only when the entities need to be expired sooner. Specify multiple such availability windows. +If an entity should only be available for a limited time, set a custom +expiration time. Without an explicit expiration time, entities will +automatically expire and be erased after 60 days. So set an expiration time only +when the entities need to be expired sooner. Specify multiple such +availability windows. val window1 = DisplayTimeWindow .Builder() @@ -251,7 +290,12 @@ If an entity should only be available for a limited time, set a custom expiratio ### DataFeedElementId -If you have integrated your Media catalogue or Media action feed with Google TV, you need not create separate entities for Movie or TV Show and instead you can create a [`MediaActionFeedEntity`](https://developer.android.com/reference/com/google/android/engage/video/datamodel/MediaActionFeedEntity) which includes the required field DataFeedElementId. This Id must be unique and must match with the ID in Media Action Feed as it helps to identify ingested feed content and perform media content lookups. +If you have integrated your Media catalogue or Media action feed with Google TV, +you need not create separate entities for Movie or TV Show and instead you can +create a [`MediaActionFeedEntity`](https://developer.android.com/reference/com/google/android/engage/video/datamodel/MediaActionFeedEntity) which includes the +required field DataFeedElementId. This Id must be unique and must match with the +ID in Media Action Feed as it helps to identify ingested feed content and +perform media content lookups. val id = "dataFeedElementId" @@ -270,7 +314,9 @@ Here's an example of creating a `MovieEntity` with all the required fields: .addTag("Action") .build() -You can provide additional data such as genres, content ratings, release date, recommendation reason and availability time windows, which may be used by Google TV for enhanced displays or filtering purposes. +You can provide additional data such as genres, content ratings, release date, +recommendation reason and availability time windows, which may be used by Google +TV for enhanced displays or filtering purposes. val genres = Arrays.asList("Action", "Science fiction"); val rating1 = RatingSystem.Builder().setAgencyName("MPAA").setRating("pg-13").build(); @@ -299,7 +345,9 @@ Here's an example of creating a `TvShowEntity` with all the required fields: .addTag("Drama") .build(); -Optionally provide additional data such as genres, content ratings, recommendation reason, offer price, season count or availability time window, which may be used by Google TV for enhanced displays or filtering purposes. +Optionally provide additional data such as genres, content ratings, +recommendation reason, offer price, season count or availability time window, +which may be used by Google TV for enhanced displays or filtering purposes. val genres = Arrays.asList("Action", "Science fiction"); val rating1 = RatingSystem.Builder() @@ -324,7 +372,8 @@ Optionally provide additional data such as genres, content ratings, recommendati ### `MediaActionFeedEntity` -Here's an example of creating an `MediaActionFeedEntity` with all the required fields: +Here's an example of creating an `MediaActionFeedEntity` with all the required +fields: val mediaActionFeedEntity = MediaActionFeedEntity.Builder() .setDataFeedElementId(id) @@ -332,7 +381,9 @@ Here's an example of creating an `MediaActionFeedEntity` with all the required f .addTag("Action") .build() -Optionally provide additional data such as description, recommendation reason and display time window, which may be used by Google TV for enhanced displays or filtering purposes. +Optionally provide additional data such as description, recommendation reason +and display time window, which may be used by Google TV for enhanced displays or +filtering purposes. val mediaActionFeedEntity = MediaActionFeedEntity.Builder() .setName("Movie name or TV Show name") @@ -343,7 +394,8 @@ Optionally provide additional data such as description, recommendation reason an ### `LiveTvChannelEntity` -This represents a live TV channel. Here's an example of creating a `LiveTvChannelEntity` with all the required fields: +This represents a live TV channel. Here's an example of creating a +`LiveTvChannelEntity` with all the required fields: val liveTvChannelEntity = LiveTvChannelEntity.Builder() .setName("Channel Name") @@ -357,7 +409,8 @@ This represents a live TV channel. Here's an example of creating a `LiveTvChanne .addTag("News") .build() -Optionally provide additional data such as content ratings or recommendation reason. +Optionally provide additional data such as content ratings or +recommendation reason. val rating1 = RatingSystem.Builder() .setAgencyName("MPAA") @@ -373,7 +426,9 @@ Optionally provide additional data such as content ratings or recommendation rea ### `LiveTvProgramEntity` -This represents a live TV program card airing or scheduled to air on a live TV channel. Here's an example of creating a `LiveTvProgramEntity` with all the required fields: +This represents a live TV program card airing or scheduled to air on +a live TV channel. Here's an example of creating a `LiveTvProgramEntity` +with all the required fields: val liveTvProgramEntity = LiveTvProgramEntity.Builder() // First set the channel information @@ -395,7 +450,8 @@ This represents a live TV program card airing or scheduled to air on a live TV c .addTag("Sports") .build() -Optionally provide additional data such as content ratings, genres, or recommendation reason. +Optionally provide additional data such as content ratings, genres, or +recommendation reason. val rating1 = RatingSystem.Builder() .setAgencyName("MPAA") diff --git a/play/engage-sdk-integration/references/android/guide/playcore/engage/watch.md b/play/engage-sdk-integration/references/android/guide/playcore/engage/watch.md index 4507511..42c9296 100644 --- a/play/engage-sdk-integration/references/android/guide/playcore/engage/watch.md +++ b/play/engage-sdk-integration/references/android/guide/playcore/engage/watch.md @@ -1,9 +1,18 @@ > [!IMPORTANT] > **Important:** Engage SDK has superseded Media Home, which is now deprecated. If you have an existing Media Home integration, follow the instructions in these guides to migrate your content to Engage SDK, which allows your content to be published to more devices and form factors. Please contact [`engage-developers@google.com`](mailto:engage-developers@google.com) if you have any questions. -Boost app engagement by reaching your users where they are. Integrate Engage SDK to deliver Continue Watching content and personalized recommendations directly to users across multiple on-device surfaces **[Collections](https://android-developers.googleblog.com/2024/07/introducing-collections-powered-by-engage-sdk.html)** , **[Entertainment Space](https://blog.google/products/android/entertainment-space/)** , and the Play Store. The integration adds less than 50 KB (compressed) to the average APK and takes most apps about a week of developer time. Learn more at our **[business site](http://play.google.com/console/about/programs/EngageSDK)**. +Boost app engagement by reaching your users where they are. Integrate Engage +SDK to deliver Continue Watching content and personalized recommendations +directly to users across multiple on-device surfaces +**[Collections](https://android-developers.googleblog.com/2024/07/introducing-collections-powered-by-engage-sdk.html)** , +**[Entertainment Space](https://blog.google/products/android/entertainment-space/)** , and the Play Store. The +integration adds less than 50 KB (compressed) to the average APK and takes +most apps about a week of developer time. Learn more at our +**[business site](http://play.google.com/console/about/programs/EngageSDK)**. -This guide contains instructions for developer partners to integrate their video content, using the Engage SDK to populate both this new surface area and existing Google surfaces. +This guide contains instructions for developer partners to integrate their video +content, using the Engage SDK to populate both this new surface area and +existing Google surfaces. > [!NOTE] > **Note:** Check the [TV integration guide](https://developer.android.com/guide/playcore/engage/tv) to learn more about the Continue Watching experience on Google TV. @@ -12,21 +21,35 @@ This guide contains instructions for developer partners to integrate their video ### Terminology -This integration includes the following three cluster types: **Recommendation** , **Continuation** , and **Featured**. +This integration includes the following three cluster types: **Recommendation** , +**Continuation** , and **Featured**. -- **Recommendation** clusters show personalized suggestions for content to watch from an individual developer partner. +- **Recommendation** clusters show personalized suggestions for content to watch + from an individual developer partner. Your recommendations take the following structure: - - **Recommendation Cluster:** A UI view that contains a group of recommendations from the same developer partner. + - **Recommendation Cluster:** A UI view that contains a group of + recommendations from the same developer partner. ![](https://developer.android.com/static/images/guide/playcore/engage/watch-term-1.png) **Figure 1.** Entertainment Space UI showing a Recommendation Cluster from a single partner. - - **Entity:** An object representing a single item in a cluster. An entity can be a movie, a TV show, a TV series, live video, and more. See the [Provide entity data](https://developer.android.com/guide/playcore/engage/watch#provide-entity-data) section for a list of supported entity types. + - **Entity:** An object representing a single item in a cluster. An entity + can be a movie, a TV show, a TV series, live video, and more. See the + [Provide entity data](https://developer.android.com/guide/playcore/engage/watch#provide-entity-data) section for a list of + supported entity types. ![](https://developer.android.com/static/images/guide/playcore/engage/watch-term-2.png) **Figure 2.** Entertainment Space UI showing a single Entity within a single partner's Recommendation Cluster. -- The **Continuation** cluster shows unfinished videos and relevant newly released episodes from multiple developer partners in one UI grouping. Each developer partner will be allowed to broadcast a maximum of 10 entities in the Continuation cluster. Research has shown that personalized recommendations along with personalized Continuation content creates the best user engagement. +- The **Continuation** cluster shows unfinished videos and relevant newly + released episodes from multiple developer partners in one UI grouping. Each + developer partner will be allowed to broadcast a maximum of 10 entities in the + Continuation cluster. Research has shown that personalized recommendations + along with personalized Continuation content creates the best user engagement. ![](https://developer.android.com/static/images/guide/playcore/engage/watch-term-3.png) **Figure 3.** Entertainment Space UI showing a Continuation cluster with unfinished recommendations from multiple partners (only one recommendation is currently visible). -- The **Featured** cluster showcases a selection of entities from multiple developer partners in one UI grouping. There will be a single Featured cluster, which is surfaced near the top of the UI with a priority placement above all Recommendation clusters. Each developer partner will be allowed to broadcast up to 10 entities in the Featured cluster. +- The **Featured** cluster showcases a selection of entities from multiple + developer partners in one UI grouping. There will be a single Featured + cluster, which is surfaced near the top of the UI with a priority placement + above all Recommendation clusters. Each developer partner will be allowed to + broadcast up to 10 entities in the Featured cluster. ![](https://developer.android.com/static/images/guide/playcore/engage/watch-term-4.png) **Figure 4.** Entertainment Space UI showing a Featured cluster with recommendations from multiple partners (only one recommendation is currently visible). @@ -41,13 +64,16 @@ Add the `com.google.android.engage:engage-core` library to your app: implementation 'com.google.android.engage:engage-core:1.6.0' } -For more information, see [Package visibility in Android 11](https://developer.android.com/about/versions/11/privacy/package-visibility). +For more information, see [Package visibility in Android +11](https://developer.android.com/about/versions/11/privacy/package-visibility). ### Summary -The design is based on an implementation of a [bound service](https://developer.android.com/guide/components/bound-services). +The design is based on an implementation of a [bound +service](https://developer.android.com/guide/components/bound-services). -The data a client can publish is subject to the following limits for different cluster types: +The data a client can publish is subject to the following limits for different +cluster types: | Cluster type | Cluster limits | Maximum entity limits in a cluster | |---|---|---| @@ -56,13 +82,16 @@ The data a client can publish is subject to the following limits for different c | Featured Cluster | At most 1 | At most 20 | > [!IMPORTANT] -> **Important:** Engage SDK has superseded Media Home, which is now deprecated. If you have an existing Media Home integration, follow the instructions in [Step 0](https://developer.android.com/guide/playcore/engage/watch#migration) to migrate your content to Engage SDK, which allows your content to be published to more devices and form factors. Contact [`engage-developers@google.com`](mailto:engage-developers@google.com). If you don't have an existing Media Home integration, skip to [Step 1](https://developer.android.com/guide/playcore/engage/watch#provide-entity-data). +> **Important:** Engage SDK has superseded Media Home, which is now deprecated. If you have an existing Media Home integration, follow the instructions in [Step +> 0](https://developer.android.com/guide/playcore/engage/watch#migration) to migrate your content to Engage SDK, which allows your content to be published to more devices and form factors. Contact [`engage-developers@google.com`](mailto:engage-developers@google.com). If you don't have an existing Media Home integration, skip to [Step +> 1](https://developer.android.com/guide/playcore/engage/watch#provide-entity-data). ### Step 0: Migration from existing Media Home SDK integration #### Map data models from existing integration -If you are migrating from an existing Media Home integration, the following table outlines how to map data models in existing SDKs to the new Engage SDK: +If you are migrating from an existing Media Home integration, the following +table outlines how to map data models in existing SDKs to the new Engage SDK: | MediaHomeVideoContract integration equivalent | Engage SDK integration equivalent | |---|---| @@ -94,7 +123,9 @@ With Media Home SDK, clusters and entities were published through separate APIs: // 5. publish new programs in the channel PreviewChannelHelper.publishPreviewProgram(builder.build()); -With Engage SDK, cluster and entity publishing are combined into a single API call. All entities that belong to a cluster are published together with that cluster: +With Engage SDK, cluster and entity publishing are combined into a single API +call. All entities that belong to a cluster are published together with that +cluster: ### Kotlin @@ -116,7 +147,8 @@ With Engage SDK, cluster and entity publishing are combined into a single API ca ### Step 1: Provide entity data -The SDK has defined different entities to represent each item type. We support the following entities for the Watch category: +The SDK has defined different entities to represent each item type. We support +the following entities for the Watch category: 1. [`MovieEntity`](https://developer.android.com/guide/playcore/engage/watch#movieentity) 2. [`TvShowEntity`](https://developer.android.com/guide/playcore/engage/watch#tvshowentity) @@ -230,7 +262,8 @@ The following chart outlines attributes and requirements for each type. #### `VideoClipEntity` -The `VideoClipEntity` object represents a video entity coming from social media, such as TikTok or YouTube. +The `VideoClipEntity` object represents a video entity coming from social media, +such as TikTok or YouTube. | Attribute | Requirement | Notes | |---|---|---| @@ -311,9 +344,13 @@ PNG, JPG, static GIF, WebP ### Step 2: Provide Cluster data -It's recommended to have the content publish job executed in the background (for example, using [WorkManager](https://developer.android.com/topic/libraries/architecture/workmanager)) and scheduled on a regular basis or on an event basis (for example, every time the user opens the app or when the user just added something to their cart). +It's recommended to have the content publish job executed in the background +(for example, using [WorkManager](https://developer.android.com/topic/libraries/architecture/workmanager)) +and scheduled on a regular basis or on an event basis (for example, every time +the user opens the app or when the user just added something to their cart). -`AppEngagePublishClient` is responsible for publishing clusters. Following APIs are available in the client: +`AppEngagePublishClient` is responsible for publishing clusters. Following +APIs are available in the client: - `isServiceAvailable` - `publishRecommendationClusters` @@ -329,11 +366,11 @@ It's recommended to have the content publish job executed in the background (for #### `isServiceAvailable` -This API is used to check if the service is available for integration and whether the content can be presented on the device. +This API is used to check if the service is available for integration and +whether the content can be presented on the device. ##### For Engage SDK v1.6.0 and higher (Recommended) -
## Android skills @@ -351,7 +388,11 @@ If your team uses AI coding tools (such as Gemini in Android Studio), you can au
-You can check the service availability for every cluster type that you intend to publish. The `isServiceAvailable` API accepts a request object, `ServiceAvailabilityRequest`, which contains the cluster types for which service availability needs to be checked. You can find the `ClusterType` enum values required for `ServiceAvailabilityRequest` from the following table. +You can check the service availability for every cluster type that you intend to +publish. The `isServiceAvailable` API accepts a request object, +`ServiceAvailabilityRequest`, which contains the cluster types for which service +availability needs to be checked. You can find the `ClusterType` enum values +required for `ServiceAvailabilityRequest` from the following table. | Cluster Type | Cluster Type Constant | Integer Value | |---|---|---| @@ -409,11 +450,25 @@ You can check the service availability for every cluster type that you intend to ###### Conditional Service Availability Feature -Some integrated apps request a special configuration that enables and disables the Engage service intermittently in order to reduce their serving cost. This intermittent content ingestion strategy, although possible, negatively affects the user and the product -- stale content will not be presented and some surfaces will not be served at all. +Some integrated apps request a special configuration that enables and disables +the Engage service intermittently in order to reduce their serving cost. This +intermittent content ingestion strategy, although possible, negatively affects +the user and the product -- stale content will not be presented and some surfaces +will not be served at all. -Starting with v1.6.0, the Engage SDK allows checking availability for specific cluster types. This provides more flexibility so that if the intermittent content strategy was adopted by a given application, some cluster types can follow that intermittent strategy while other cluster types are always enabled (i.e. continuation clusters). +Starting with v1.6.0, the Engage SDK allows checking availability for specific +cluster types. This provides more flexibility so that if the intermittent +content strategy was adopted by a given application, some cluster types can +follow that intermittent strategy while other cluster types are always enabled +(i.e. continuation clusters). -If the Engage service should not be 'continuously' enabled on all supported devices for whatever reason, and is configured for intermittent ingestion for any set of devices, all continuation cluster publications (e.g. Continue Watching) will be still enabled by default configuration, and the rest of the cluster types will be enabled and disabled intermittently. If intermittent ingestion applies to you but this default configuration is not suitable for your needs, please contact engage-developers@google.com. +If the Engage service should not be 'continuously' enabled on all supported +devices for whatever reason, and is configured for intermittent ingestion for +any set of devices, all continuation cluster publications (e.g. Continue +Watching) will be still enabled by default configuration, and the rest of the +cluster types will be enabled and disabled intermittently. If intermittent +ingestion applies to you but this default configuration is not suitable for +your needs, please contact engage-developers@google.com. ##### For SDK versions prior to v1.6.0 (Deprecated) @@ -487,12 +542,14 @@ This API is used to publish a list of `RecommendationCluster` objects. .build()) .build()); -When the service receives the request, the following actions take place within one transaction: +When the service receives the request, the following actions take place within +one transaction: - Existing `RecommendationCluster` data from the developer partner is removed. - Data from the request is parsed and stored in the updated Recommendation Cluster. -In case of an error, the entire request is rejected and the existing state is maintained. +In case of an error, the entire request is rejected and the existing state is +maintained. #### `publishFeaturedCluster` @@ -523,12 +580,14 @@ This API is used to publish a list of `FeaturedCluster` objects. .build()) .build()); -When the service receives the request, the following actions take place within one transaction: +When the service receives the request, the following actions take place within +one transaction: - Existing `FeaturedCluster` data from the developer partner is removed. - Data from the request is parsed and stored in the updated Featured Cluster. -In case of an error, the entire request is rejected and the existing state is maintained. +In case of an error, the entire request is rejected and the existing state is +maintained. #### `publishContinuationCluster` @@ -559,16 +618,20 @@ This API is used to publish a `ContinuationCluster` object. .build()) .build()); -When the service receives the request, the following actions take place within one transaction: +When the service receives the request, the following actions take place within +one transaction: - Existing `ContinuationCluster` data from the developer partner is removed. - Data from the request is parsed and stored in the updated Continuation Cluster. -In case of an error, the entire request is rejected and the existing state is maintained. +In case of an error, the entire request is rejected and the existing state is +maintained. #### `publishUserAccountManagementRequest` -This API is used to publish a Sign In card . The signin action directs users to the app's sign in page so that the app can publish content (or provide more personalized content) +This API is used to publish a Sign In card . The signin action directs users to +the app's sign in page so that the app can publish content (or provide more +personalized content) The following metadata is part of the Sign In Card - @@ -621,16 +684,21 @@ The following metadata is part of the Sign In Card - .setSignInCardEntity(SIGN_IN_CARD_ENTITY) .build()); -When the service receives the request, the following actions take place within one transaction: +When the service receives the request, the following actions take place within +one transaction: - Existing `UserAccountManagementCluster` data from the developer partner is removed. - Data from the request is parsed and stored in the updated UserAccountManagementCluster Cluster. -In case of an error, the entire request is rejected and the existing state is maintained. +In case of an error, the entire request is rejected and the existing state is +maintained. #### `updatePublishStatus` -If for any internal business reason, none of the clusters is published, we **strongly recommend** updating the publish status using the **updatePublishStatus** API. This is important because : +If for any internal business reason, none of the clusters is published, +we **strongly recommend** updating the publish status using the +**updatePublishStatus** API. +This is important because : - Providing the status in all scenarios, even when the content is published (STATUS == PUBLISHED), is critical to populate dashboards that use this explicit status to convey the health and other metrics of your integration. - If no content is published but the integration status isn't broken (STATUS == NOT_PUBLISHED), Google can avoid triggering alerts in the app health dashboards. It confirms that content is not published due to an **expected** situation from the provider's standpoint. @@ -670,7 +738,11 @@ The list of eligible publish status codes are : // Reach out to engage-developers@ before using this enum. AppEngagePublishStatusCode.NOT_PUBLISHED_OTHER -If the content is not published due to a user not logged in, Google would recommend publishing the Sign In Card. If for any reason providers are not able to publish the Sign In Card then we recommend calling the **updatePublishStatus** API with the status code **NOT_PUBLISHED_REQUIRES_SIGN_IN** +If the content is not published due to a user not logged in, +Google would recommend publishing the Sign In Card. +If for any reason providers are not able to publish the Sign In Card +then we recommend calling the **updatePublishStatus** API +with the status code **NOT_PUBLISHED_REQUIRES_SIGN_IN** ### Kotlin @@ -701,7 +773,9 @@ This API is used to delete the content of Recommendation Clusters. client.deleteRecommendationClusters(); -When the service receives the request, it removes the existing data from the Recommendation Clusters. In case of an error, the entire request is rejected and the existing state is maintained. +When the service receives the request, it removes the existing data from the +Recommendation Clusters. In case of an error, the entire request is rejected +and the existing state is maintained. > [!NOTE] > **Note:** This api is available from version 1.1.0 onwards. @@ -721,7 +795,9 @@ This API is used to delete the content of Featured Cluster. client.deleteFeaturedCluster(); -When the service receives the request, it removes the existing data from the Featured Cluster. In case of an error, the entire request is rejected and the existing state is maintained. +When the service receives the request, it removes the existing data from the +Featured Cluster. In case of an error, the entire request is rejected +and the existing state is maintained. > [!NOTE] > **Note:** This api is available from version 1.1.0 onwards. @@ -741,7 +817,9 @@ This API is used to delete the content of Continuation Cluster. client.deleteContinuationCluster(); -When the service receives the request, it removes the existing data from the Continuation Cluster. In case of an error, the entire request is rejected and the existing state is maintained. +When the service receives the request, it removes the existing data from the +Continuation Cluster. In case of an error, the entire request is rejected +and the existing state is maintained. > [!NOTE] > **Note:** This api is available from version 1.1.0 onwards. @@ -761,7 +839,9 @@ This API is used to delete the content of UserAccountManagement Cluster. client.deleteUserManagementCluster(); -When the service receives the request, it removes the existing data from the UserAccountManagement Cluster. In case of an error, the entire request is rejected and the existing state is maintained. +When the service receives the request, it removes the existing data from the +UserAccountManagement Cluster. In case of an error, the entire request is +rejected and the existing state is maintained. > [!NOTE] > **Note:** This api is available from version 1.1.0 onwards. @@ -791,11 +871,15 @@ This API is used to delete the content of a given cluster type. .addClusterType(ClusterType.TYPE_RECOMMENDATION) .build()); -When the service receives the request, it removes the existing data from all clusters matching the specified cluster types. Clients can choose to pass one or many cluster types. In case of an error, the entire request is rejected and the existing state is maintained. +When the service receives the request, it removes the existing data from all +clusters matching the specified cluster types. Clients can choose to pass one or +many cluster types. In case of an error, the entire request is rejected and the +existing state is maintained. #### Error handling -It is highly recommended to listen to the task result from the publish APIs such that a follow-up action can be taken to recover and resubmit an successful task. +It is highly recommended to listen to the task result from the publish APIs such +that a follow-up action can be taken to recover and resubmit an successful task. ### Kotlin @@ -839,7 +923,8 @@ It is highly recommended to listen to the task result from the publish APIs such } }); -The error is returned as an `AppEngageException` with the cause included as an error code. +The error is returned as an `AppEngageException` with the cause included as an +error code. | Error code | Error name | Note | |---|---|---| @@ -853,13 +938,23 @@ The error is returned as an `AppEngageException` with the cause included as an e ### Step 3: Handle broadcast intents -In addition to making publish content API calls through a job, it is also required to set up a [`BroadcastReceiver`](https://developer.android.com/reference/android/content/BroadcastReceiver) to receive the request for a content publish. +In addition to making publish content API calls through a job, it is also +required to set up a +[`BroadcastReceiver`](https://developer.android.com/reference/android/content/BroadcastReceiver) to receive +the request for a content publish. -The goal of broadcast intents is mainly for app reactivation and forcing data sync. Broadcast intents are not designed to be sent very frequently. It is only triggered when the Engage Service determines the content might be stale (for example, a week old). That way, there is more confidence that the user can have a fresh content experience, even if the application has not been executed for a long period of time. +The goal of broadcast intents is mainly for app reactivation and forcing data +sync. Broadcast intents are not designed to be sent very frequently. It is only +triggered when the Engage Service determines the content might be stale (for +example, a week old). That way, there is more confidence that the user can have +a fresh content experience, even if the application has not been executed for a +long period of time. The `BroadcastReceiver` must be set up in the following two ways: -- Dynamically register an instance of the `BroadcastReceiver` class using `Context.registerReceiver()`. This enables communication from applications that are still live in memory. +- Dynamically register an instance of the `BroadcastReceiver` class using + `Context.registerReceiver()`. This enables communication from applications + that are still live in memory. ### Kotlin @@ -930,7 +1025,10 @@ The `BroadcastReceiver` must be set up in the following two ways: } -- Statically declare an implementation with the `` tag in your `AndroidManifest.xml` file. This allows the application to receive broadcast intents when it is not running, and also allows the application to publish the content. +- Statically declare an implementation with the `` tag in your + `AndroidManifest.xml` file. This allows the application to receive broadcast + intents when it is not running, and also allows the application to publish + the content. -The following [intents](https://developer.android.com/reference/android/content/Intent) is sent by the service: +The following [intents](https://developer.android.com/reference/android/content/Intent) is sent by the +service: - `com.google.android.engage.action.PUBLISH_RECOMMENDATION` It is recommended to start a `publishRecommendationClusters` call when receiving this intent. - `com.google.android.engage.action.PUBLISH_FEATURED` It is recommended to start a `publishFeaturedCluster` call when receiving this intent. @@ -958,15 +1057,19 @@ The following [intents](https://developer.android.com/reference/android/content/ ## Integration workflow -For a step-by-step guide on verifying your integration after it is complete, see [Engage developer integration workflow](https://developer.android.com/guide/playcore/engage/workflow). +For a step-by-step guide on verifying your integration after it is complete, see +[Engage developer integration workflow](https://developer.android.com/guide/playcore/engage/workflow). ## FAQs -See [Engage SDK Frequently Asked Questions](https://developer.android.com/guide/playcore/engage/faq) for FAQs. +See [Engage SDK Frequently Asked Questions](https://developer.android.com/guide/playcore/engage/faq) for +FAQs. ## Contact -Contact [`engage-developers@google.com`](mailto:engage-developers@google.com) if there are any questions during the integration process. +Contact +[`engage-developers@google.com`](mailto:engage-developers@google.com) if there are +any questions during the integration process. ## Next steps diff --git a/play/engage-sdk-integration/references/clusters.md b/play/engage-sdk-integration/references/clusters.md index efe5025..f8976bb 100644 --- a/play/engage-sdk-integration/references/clusters.md +++ b/play/engage-sdk-integration/references/clusters.md @@ -23,7 +23,8 @@ The Engage SDK defines cluster structures as shown in this reference: "adder": "addAllDisplayTimeWindow(DisplayTimeWindow)", "adderAll": "addAllDisplayTimeWindow(List)" } - } + }, + "associatedClusterType": "ClusterType.TYPE_FEATURED" }, "ContinuationCluster": { "package": "com.google.android.engage.common.datamodel.ContinuationCluster", @@ -58,7 +59,8 @@ The Engage SDK defines cluster structures as shown in this reference: "adder": "addAllDisplayTimeWindow(DisplayTimeWindow)", "adderAll": "addAllDisplayTimeWindow(List)" } - } + }, + "associatedClusterType": "ClusterType.TYPE_CONTINUATION" }, "RecommendationCluster": { "package": "com.google.android.engage.common.datamodel.RecommendationCluster", @@ -111,7 +113,8 @@ The Engage SDK defines cluster structures as shown in this reference: "setter": "setRecommendationClusterType(@RecommendationClusterType int)", "getter": "getRecommendationClusterType()" } - } + }, + "associatedClusterType": "ClusterType.TYPE_RECOMMENDATION" }, "SubscriptionCluster": { "package": "com.google.android.engage.common.datamodel.SubscriptionCluster", @@ -128,10 +131,12 @@ The Engage SDK defines cluster structures as shown in this reference: "adder": "addSubscriptionEntity(SubscriptionEntity)", "getter": "getSubscriptionEntities()" } - } + }, + "associatedClusterType": "ClusterType.TYPE_SUBSCRIPTION" }, "EngagementCluster": { "package": "com.google.android.engage.common.datamodel.EngagementCluster", + "associatedClusterType": "ClusterType.TYPE_ENGAGEMENT", "fields": { "signInCardEntity": { "type": "SignInCardEntity", diff --git a/play/engage-sdk-integration/references/patterns.md b/play/engage-sdk-integration/references/patterns.md index 76d8adc..099c410 100644 --- a/play/engage-sdk-integration/references/patterns.md +++ b/play/engage-sdk-integration/references/patterns.md @@ -1,10 +1,12 @@ ## EngageBroadcastReceiver -Setting up the `BroadcastReceiver` correctly requires **both** static and dynamic registration. Static registration allows the app to receive broadcasts even when it isn't running, while dynamic registration is required on newer Android versions to safely receive broadcasts when the app is live in memory. +Setting up the `BroadcastReceiver` correctly requires **both** static and +dynamic registration. Static registration allows the app to receive broadcasts +even when it isn't running, while dynamic registration is required on newer +Android versions to safely receive broadcasts when the app is live in memory. ### BroadcastReceiver implementation -
```kotlin import android.content.BroadcastReceiver @@ -63,16 +65,15 @@ class EngageBroadcastReceiver : BroadcastReceiver() { } } } - ```
### Static registration in AndroidManifest.xml -Add the `` tag inside the `` block in `AndroidManifest.xml` +Add the `` tag inside the `` block in +`AndroidManifest.xml` -
```xml @@ -89,14 +90,12 @@ Add the `` tag inside the `` block in `AndroidManifest.xm - ```
## EngageWorker -
```kotlin import android.content.Context @@ -295,14 +294,12 @@ class EngageWorker(context: Context, workerParams: WorkerParameters) : Coroutine } } } - ```
## ClusterRequestFactory -
```kotlin class ClusterRequestFactory(context: Context) { @@ -356,14 +353,12 @@ class ClusterRequestFactory(context: Context) { .setSignInCardEntity(signInCard) .build() } - ```
## EngagePublisher -
```kotlin object EngagePublisher { @@ -383,14 +378,12 @@ object EngagePublisher { WorkManager.getInstance(context).enqueueUniqueWork("EngageOneTime", ExistingWorkPolicy.REPLACE, workRequest) } } - ```
## Constants -
```kotlin object Constants { @@ -406,14 +399,12 @@ object Constants { // const val PUBLISH_TYPE_FOOD_SHOPPING_CARD = "FOOD_SHOPPING_CARD" // const val PUBLISH_TYPE_RESERVATION = "RESERVATION" } - ```
## ItemToEntityConverter -
```kotlin object ItemToEntityConverter { @@ -428,16 +419,22 @@ object ItemToEntityConverter { .build() } } - ```
-> **Strict image aspect ratio requirement** : Play Engage has strict requirements for image aspect ratios depending on the vertical and entity type (e.g., 16:9 for landscape, 1:1 for square, 2:3 for portrait). Ensure your `ItemToEntityConverter` maps images that conform to these strict requirements to avoid cropping or content rejection by Play. +> **Strict image aspect ratio requirement** : Play Engage has strict +> requirements for image aspect ratios depending on the vertical and entity type +> (e.g., 16:9 for landscape, 1:1 for square, 2:3 for portrait). Ensure your +> `ItemToEntityConverter` maps images that conform to these strict requirements +> to avoid cropping or content rejection by Play. ## Dependency specifications (libs.versions.toml) -This skill specifies all dependencies using the `libs.versions.toml` format. Adapt these definitions to other formats (such as standard Groovy `build.gradle` or Kotlin DSL `build.gradle.kts` implementation lines) as required by the project. +This skill specifies all dependencies using the `libs.versions.toml` format. +Adapt these definitions to other formats (such as standard Groovy `build.gradle` +or Kotlin DSL `build.gradle.kts` implementation lines) as required by the +project. [versions] engage-core = "1.6.0" @@ -458,9 +455,10 @@ This skill specifies all dependencies using the `libs.versions.toml` format. Ada ## Dual content rating fields for Watch and TV -For Watch and TV integrations, you must populate both the new `contentRatings` (which uses `RatingSystem`) and the legacy `contentRatingsLegacies` (which uses `String` lists) to ensure compatibility across all Google Play surfaces. +For Watch and TV integrations, you must populate both the new `contentRatings` +(which uses `RatingSystem`) and the legacy `contentRatingsLegacies` (which uses +`String` lists) to ensure compatibility across all Google Play surfaces. -
```kotlin fun convertMovie(movie: MovieData): MovieEntity { @@ -476,30 +474,27 @@ fun convertMovie(movie: MovieData): MovieEntity { .addContentRatingsLegacy(listOf("MPAA:PG-13")) // Legacy API for backward compatibility .build() } - ```
## TV integrations -The following patterns and configurations are specific to Android TV integrations. +The following patterns and configurations are specific to Android TV +integrations. ### AndroidManifest.xml for TV -
```xml - ```
### PlatformSpecificUri example -
```kotlin val platformSpecificPlaybackUris = listOf( @@ -512,14 +507,12 @@ val platformSpecificPlaybackUris = listOf( .setActionUri(Uri.parse("https://www.example.com/mobile/play/123")) .build() ) - ```
### AccountProfile example -
```kotlin val accountProfile: AccountProfile @@ -529,7 +522,6 @@ val accountProfile: AccountProfile // AppCompatDelegate.getApplicationLocales().get(0) for Per-App Language Preferences .setLocale(Locale.getDefault().toLanguageTag()) .build() - ```
\ No newline at end of file diff --git a/play/engage-sdk-integration/references/requests.md b/play/engage-sdk-integration/references/requests.md index f841032..0975764 100644 --- a/play/engage-sdk-integration/references/requests.md +++ b/play/engage-sdk-integration/references/requests.md @@ -1,4 +1,5 @@ -Defines the request structures for publishing various data models in the Engage SDK. +Defines the request structures for publishing various data models in +the Engage SDK. { "PublishRecommendationClustersRequest": { @@ -23,7 +24,8 @@ Defines the request structures for publishing various data models in the Engage "setter": "setSyncAcrossDevices(boolean)", "getter": "getSyncAcrossDevices()" } - } + }, + "associatedClusterType": "ClusterType.TYPE_RECOMMENDATION" }, "PublishFeaturedClusterRequest": { "package": "com.google.android.engage.service.PublishFeaturedClusterRequest", @@ -34,7 +36,8 @@ Defines the request structures for publishing various data models in the Engage "setter": "setFeaturedCluster(FeaturedCluster)", "getter": "getFeaturedCluster()" } - } + }, + "associatedClusterType": "ClusterType.TYPE_FEATURED" }, "DeleteClustersRequest": { "package": "com.google.android.engage.service.DeleteClustersRequest", @@ -42,7 +45,8 @@ Defines the request structures for publishing various data models in the Engage "clusterTypes": { "type": "List<@ClusterType int>", "requirement": "Optional", - "adder": "addClusterType(@ClusterType int)" + "adder": "addClusterType(@ClusterType int)", + "description": "The ClusterType enum values for the clusters being deleted. Use the 'associatedClusterType' from the respective publish request or cluster definition." }, "deleteReason": { "type": "@DeleteReason int", @@ -73,7 +77,8 @@ Defines the request structures for publishing various data models in the Engage "setter": "setContinuationCluster(ContinuationCluster)", "getter": "getContinuationCluster()" } - } + }, + "associatedClusterType": "ClusterType.TYPE_CONTINUATION" }, "PublishStatusRequest": { "package": "com.google.android.engage.service.PublishStatusRequest", @@ -105,10 +110,12 @@ Defines the request structures for publishing various data models in the Engage "type": "SubscriptionEntity", "getter": "getSubscription()" } - } + }, + "associatedClusterType": "ClusterType.TYPE_SUBSCRIPTION" }, "PublishUserAccountManagementRequest": { "package": "com.google.android.engage.service.PublishUserAccountManagementRequest", + "associatedClusterType": "ClusterType.TYPE_ENGAGEMENT", "fields": { "actionUri": { "type": "Uri", @@ -135,7 +142,8 @@ Defines the request structures for publishing various data models in the Engage "type": "ShoppingCart", "getter": "getShoppingCart()" } - } + }, + "associatedClusterType": "ClusterType.TYPE_SHOPPING_CART" }, "PublishShoppingListsRequest": { "package": "com.google.android.engage.shopping.service.PublishShoppingListsRequest", @@ -147,7 +155,8 @@ Defines the request structures for publishing various data models in the Engage "getter": "getShoppingLists()", "adderAll": "addShoppingLists(List)" } - } + }, + "associatedClusterType": "ClusterType.TYPE_SHOPPING_LIST" }, "PublishShoppingOrderTrackingClusterRequest": { "package": "com.google.android.engage.shopping.service.PublishShoppingOrderTrackingClusterRequest", @@ -158,7 +167,8 @@ Defines the request structures for publishing various data models in the Engage "setter": "setShoppingOrderTrackingCluster(ShoppingOrderTrackingCluster)", "getter": "getShoppingOrderTrackingCluster()" } - } + }, + "associatedClusterType": "ClusterType.TYPE_SHOPPING_ORDER_TRACKING" }, "PublishShoppingReorderClusterRequest": { "package": "com.google.android.engage.shopping.service.PublishShoppingReorderClusterRequest", @@ -169,7 +179,8 @@ Defines the request structures for publishing various data models in the Engage "setter": "setReorderCluster(ShoppingReorderCluster)", "getter": "getReorderCluster()" } - } + }, + "associatedClusterType": "ClusterType.TYPE_SHOPPING_REORDER" }, "PublishFoodShoppingCartsRequest": { "package": "com.google.android.engage.food.service.PublishFoodShoppingCartsRequest", @@ -181,7 +192,8 @@ Defines the request structures for publishing various data models in the Engage "getter": "getFoodShoppingCarts()", "adderAll": "addFoodShoppingCarts(List)" } - } + }, + "associatedClusterType": "ClusterType.TYPE_FOOD_SHOPPING_CART" }, "PublishFoodShoppingListsRequest": { "package": "com.google.android.engage.food.service.PublishFoodShoppingListsRequest", @@ -193,7 +205,8 @@ Defines the request structures for publishing various data models in the Engage "getter": "getFoodShoppingLists()", "adderAll": "addFoodShoppingLists(List)" } - } + }, + "associatedClusterType": "ClusterType.TYPE_FOOD_SHOPPING_LIST" }, "PublishReorderClusterRequest": { "package": "com.google.android.engage.food.service.PublishReorderClusterRequest", @@ -204,7 +217,8 @@ Defines the request structures for publishing various data models in the Engage "type": "FoodReorderCluster", "getter": "getReorderCluster()" } - } + }, + "associatedClusterType": "ClusterType.TYPE_FOOD_REORDER" }, "PublishContinueSearchClusterRequest": { "package": "com.google.android.engage.travel.service.PublishContinueSearchClusterRequest", @@ -215,7 +229,8 @@ Defines the request structures for publishing various data models in the Engage "setter": "setContinueSearchCluster(ContinueSearchCluster)", "getter": "getContinueSearchCluster()" } - } + }, + "associatedClusterType": "ClusterType.TYPE_CONTINUE_SEARCH" }, "PublishReservationClusterRequest": { "package": "com.google.android.engage.travel.service.PublishReservationClusterRequest", @@ -226,7 +241,8 @@ Defines the request structures for publishing various data models in the Engage "setter": "setReservationCluster(ReservationCluster)", "getter": "getReservationCluster()" } - } + }, + "associatedClusterType": "ClusterType.TYPE_RESERVATION" }, "ServiceAvailabilityRequest": { "package": "com.google.android.engage.service.ServiceAvailabilityRequest", @@ -236,7 +252,8 @@ Defines the request structures for publishing various data models in the Engage "requirement": "Required", "adder": "addIntendedClusterType(@ClusterType int)", "adderAll": "addAllIntendedClusterTypes(List)", - "getter": "getIntendedClusterTypes()" + "getter": "getIntendedClusterTypes()", + "description": "The ClusterType enum values for the clusters being published (e.g. ClusterType.TYPE_ENGAGEMENT). Use the 'associatedClusterType' from the respective publish request or cluster definition." } } } diff --git a/play/engage-sdk-integration/references/troubleshooting.md b/play/engage-sdk-integration/references/troubleshooting.md new file mode 100644 index 0000000..c47d02b --- /dev/null +++ b/play/engage-sdk-integration/references/troubleshooting.md @@ -0,0 +1,39 @@ +This guide helps resolve common compilation, import, and sandbox-related errors encountered during the Play Engage SDK integration. + +## 1. Ensure sandbox compilation safety for agents + +When executing compilations in sandboxed or ephemeral workspaces, such as evaluation environments or automated presubmits, you must follow these rules to prevent container crashes and lockups: + +- **Use full compilation** : Run `./gradlew compileDebugUnitTestSources --no-daemon` or `./gradlew assembleDebug --no-daemon`. +- **Don't use compilation shortcuts** : Avoid commands like `./gradlew :app:compileDebugKotlin` or other compile-only tasks. These shortcuts bypass manifest merging and resource packaging, which can hide critical errors like namespace conflicts or XML syntax errors. +- **Don't run full tests or builds** : Don't run `./gradlew test` or `./gradlew build`. These tasks trigger heavy testing environments (requiring emulators) and run full builds, which easily crash the FUSE filesystem mounts and exhaust sandbox CPU and RAM. +- **Always use --no-daemon** : Always append `--no-daemon` to prevent background JVM processes from leaking and locking files in the sandbox. + +## 2. Resolve missing imports + +If you're upgrading the SDK version, for example, to 1.6.0, or if you get `Unresolved reference` errors during the initial compilation, make sure you've added the correct import statements: + +| Class name | Package or import statement | Note | +|---|---|---| +| `ImageTheme` | `import com.google.android.engage.common.datamodel.ImageTheme` | | +| `PlatformSpecificUri` | `import com.google.android.engage.common.datamodel.PlatformSpecificUri` | | +| `DisplayTimeWindow` | `import com.google.android.engage.common.datamodel.DisplayTimeWindow` | | +| `RatingSystem` | `import com.google.android.engage.video.datamodel.RatingSystem` | Note: It's under `video.datamodel`, not `common.datamodel`. | + +## 3. Resolve namespace conflicts + +You might receive an error message like this one: + + Namespace 'com.google.android.engage' is used in multiple modules and/or libraries: com.google.android.engage:engage-core:1.6.0, com.google.android.engage:engage-tv:1.1.0 + +This conflict occurs because both packages declare the same namespace. +\* **Resolution** : Remove the explicit `engage-core` dependency from your `build.gradle` or `build.gradle.kts` and keep *only* `engage-tv`. The `engage-tv` library transitively pulls in the compatible version of `engage-core` automatically. + +## 4. Resolve sandbox-specific build and test failures + +When compiling or running tests in the evaluation sandbox (especially for large projects like Now in Android), you might encounter environment-specific failures: + +- **Jlink error with Android SDK 36** : If the build fails during Java compilation with a `jlink` error in `JdkImageTransform` for `android-36`, force Gradle to use JDK 17 by prepending `JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64` to the Gradle command. +- **Missing ANDROID_HOME** : If lint or test tasks fail complaining about missing SDK, explicitly set the environment variable `ANDROID_HOME=/usr/local/google/home/shashvatgupta/Android/Sdk` (or the corresponding path on the workstation). +- **Roborazzi screenshot mismatches** : Headless or sandbox container rendering might differ from local golden images, causing Roborazzi screenshot test assertions to fail. Bypass these visual checks by appending `-Proborazzi.test.verify=false` to your test command. +- **Robolectric SDK 36 unsupported** : If screenshot tests fail with `java.lang.UnsupportedOperationException` in `DefaultSdkProvider` due to experimental SDK 36, update the `@Config` annotation in the failing test files to pin the SDK to 34 (e.g., `@Config(..., sdk = [34])`). \ No newline at end of file diff --git a/play/play-billing-library-version-upgrade/SKILL.md b/play/play-billing-library-version-upgrade/SKILL.md index 8c96d85..3174a11 100644 --- a/play/play-billing-library-version-upgrade/SKILL.md +++ b/play/play-billing-library-version-upgrade/SKILL.md @@ -6,7 +6,7 @@ description: Use this skill when upgrading or migrating an Android project from license: Complete terms in LICENSE.txt metadata: author: Google LLC - last-updated: '2026-08-06' + last-updated: '2026-08-14' keywords: - android - play billing @@ -20,7 +20,8 @@ metadata: ## Phase 0: Intent Message -**Reporting Action**: Before proceeding, immediately tell the user: "I will upgrade Play Billing Library to the latest version." +**Reporting Action**: Before proceeding, immediately tell the user: "I will +upgrade Play Billing Library to the latest version." ## Phase 1: Discovery \& Situational Awareness @@ -28,18 +29,22 @@ metadata: 2. **Initial Compilation Test**: Attempt to sync and build the project immediately. 3. **Fallback Discovery (Effective Version)** : - **Trigger**: Only if the build fails immediately, scan the source code for deprecated artifacts. - - **Logic** : The presence of deprecated APIs indicates the **"Effective Version"** ---defined as the version where those specific APIs were **last available**, not when they were introduced. + - **Logic** : The presence of deprecated APIs indicates the **"Effective + Version"** ---defined as the version where those specific APIs were **last + available**, not when they were introduced. - **Example** : If `SkuDetails` is present, treat the baseline as **PBL v7** or earlier (regardless of the version string in `build.gradle`). 4. **Identify Target \& Path** : Access the version tool or release notes to find the latest stable version and calculate a \[Direct/Stepped\] migration path based on the **Effective Version** baseline. - **Calculate Migration Path** : - If the **Effective Version** is within 2 major versions of the target: Plan a **Direct Migration**. - - If it is more than 2 major versions behind: Plan a **Stepped Migration**. Migrate by two major versions at a time (e.g., v4 -\> v6 -\> v8) until you are within two versions of the target. + - If it is more than 2 major versions behind: Plan a **Stepped + Migration**. Migrate by two major versions at a time (e.g., v4 -\> v6 -\> v8) until you are within two versions of the target. - **Reporting Action**: Before proceeding, tell the user: "I've detected you are effectively on PBL \[Current\] and the latest is \[Target\]. I am planning a \[direct/stepped\] migration path." ## Phase 2: Contextual Document Mapping \& Planning -For every major version jump identified in your path, you **MUST** synthesize instructions from: +For every major version jump identified in your path, you **MUST** synthesize +instructions from: - **[Migration Guide](https://developer.android.com/google/play/billing/migrate-gpblv%5BX%5D)** (where `[X]` is the target major version). - **Release Highlights** : The "Deprecations" and "Breaking Changes" sections of the relevant [Release Notes](references/android/google/play/billing/release-notes.md). @@ -48,7 +53,9 @@ For every major version jump identified in your path, you **MUST** synthesize in ## Phase 3: Instructions for Execution -*Reporting Action: For each of the following steps, give a brief explanation of what you will be doing prior to execution, and a brief summary of what you accomplished afterwards.* +*Reporting Action: For each of the following steps, give a brief explanation of +what you will be doing prior to execution, and a brief summary of what you +accomplished afterwards.* ### Step 1: SDK \& Environment Alignment @@ -57,7 +64,8 @@ For every major version jump identified in your path, you **MUST** synthesize in ### Step 2: Intent-based Refactoring -Analyze the intent of the existing code rather than performing purely textual string replacement. +Analyze the intent of the existing code rather than performing purely textual +string replacement. - **Action** : You **MUST** follow all deprecation instructions and refactor patterns from **both** the [references/migration-logic.md](references/migration-logic.md) section, the official migration guides, and the general documentation pages identified in Phase 2. - **Verification** : Verify you are doing **all steps from all documentation** and then making sure you follow the specific directions from the checklist in the references. diff --git a/play/play-billing-library-version-upgrade/references/android/google/play/billing/release-notes.md b/play/play-billing-library-version-upgrade/references/android/google/play/billing/release-notes.md index 1d1105c..ae3ff26 100644 --- a/play/play-billing-library-version-upgrade/references/android/google/play/billing/release-notes.md +++ b/play/play-billing-library-version-upgrade/references/android/google/play/billing/release-notes.md @@ -1,7 +1,21 @@ This document contains release notes for the Google Play Billing Library. -> [!NOTE] -> **Note:** You can use the [Play Billing Library Version Upgrade Skill](https://github.com/android/skills/tree/main/play/play-billing-library-version-upgrade) to automate your upgrade to the latest version. + +## Android skills + +[View on GitHub](https://github.com/android/skills/tree/main/play/play-billing-library-version-upgrade) + +### Upgrade Play Billing Library + +Use the Play Billing Library [Android skill](https://developer.android.com/tools/agents/android-skills) to automate your upgrade to the latest version. To install the skill from the [Android CLI](https://developer.android.com/tools/agents/android-cli), run: + + android skills add --skill play-billing-library-version-upgrade + +To activate the skill, try the following prompt: + + Help me upgrade my Play Billing Library implementation. + +
## Google Play Billing Library 9.1.0 Release (2026-06-18) @@ -23,21 +37,37 @@ Version 9.1.0 of the Google Play Billing Library and Kotlin extensions are now a ## Google Play Billing Library 9.0.0 Release (2026-05-19) -Version 9.0.0 of the Google Play Billing Library and Kotlin extensions are now available. See the [PBL 9 migration guide](https://developer.android.com/google/play/billing/migrate-pbl-latest) if you want to migrate from the previous versions of PBL. +Version 9.0.0 of the Google Play Billing Library and Kotlin extensions are +now available. See the [PBL 9 migration guide](https://developer.android.com/google/play/billing/migrate-pbl-latest) if you want to migrate from +the previous versions of PBL. ### Summary of changes -- **Updated error codes for blocked Play Store activity** : Error codes for blocked Play Store apps have been updated. For instances where the Play Store app is blocked by the system (for example, in OEM-customized kids mode), the response code has changed from `ERROR` to `BILLING_UNAVAILABLE`. Additionally, the `BillingResult` for such cases now provides a *Play Store is blocked* debug message. +- **Updated error codes for blocked Play Store activity** : Error codes for + blocked Play Store apps have been updated. For instances where + the Play Store app is blocked by the system + (for example, in OEM-customized kids mode), the response code has + changed from `ERROR` to `BILLING_UNAVAILABLE`. Additionally, the + `BillingResult` for such cases now provides a *Play Store is blocked* + debug message. > [!NOTE] > **Note:** For this feature to work, you need [AndroidX.core library](https://developer.android.com/jetpack/androidx/releases/core#core_and_core-ktx_version_190_2) version 1.9 or later. -- **Nullability update for developer-provided billing** : The [`DeveloperProvidedBillingDetails.getLinkUri()`](https://developer.android.com/reference/com/android/billingclient/api/DeveloperProvidedBillingDetails#getLinkUri()) method has been updated to be `@Nullable`. This change supports scenarios where the direct link URI for external payments is unavailable during the payment selection stage. +- **Nullability update for developer-provided billing** : The + [`DeveloperProvidedBillingDetails.getLinkUri()`](https://developer.android.com/reference/com/android/billingclient/api/DeveloperProvidedBillingDetails#getLinkUri()) method has + been updated to be `@Nullable`. This change supports scenarios where + the direct link URI for external payments is unavailable during the + payment selection stage. - To handle this change safely, ensure your integration code handles both `null` and empty string (`""`) values from the [`DeveloperProvidedBillingDetails.getLinkUri()`](https://developer.android.com/reference/com/android/billingclient/api/DeveloperProvidedBillingDetails#getLinkUri()) method before parsing or launching browser intents. + To handle this change safely, ensure your integration code handles both + `null` and empty string (`""`) values from the + [`DeveloperProvidedBillingDetails.getLinkUri()`](https://developer.android.com/reference/com/android/billingclient/api/DeveloperProvidedBillingDetails#getLinkUri()) method before parsing + or launching browser intents. - Updated `targetSdkVersion` to 35. -- You can now use [in-app messaging](https://developer.android.com/google/play/billing/subscriptions#in-app-messaging) to notify users of an upcoming opt-in price increase. This lets users confirm the price increase without leaving the app. The message for an outstanding opt-in price increase is shown starting on the first day the user can accept the price increase, and the message is shown a maximum of once every 7 days. +- You can now use [in-app messaging](https://developer.android.com/google/play/billing/subscriptions#in-app-messaging) to notify users + of an upcoming opt-in price increase. This lets users confirm the price increase without leaving the app. The message for an outstanding opt-in price increase is shown starting on the first day the user can accept the price increase, and the message is shown a maximum of once every 7 days. ## Google Play Billing Library 8.3.0 Release (2025-12-23) @@ -58,7 +88,8 @@ Version 8.3.0 of the Google Play Billing Library and Kotlin extensions are now a ## Google Play Billing Library 8.2.1 Release (2025-12-15) -Version 8.2.1 of the Google Play Billing Library and Kotlin extensions are now available. +Version 8.2.1 of the Google Play Billing Library and Kotlin extensions are now +available. ### Bug fixes @@ -92,29 +123,46 @@ Version 8.1.0 of the Google Play Billing Library and Kotlin extensions are now a - Suspended subscriptions - A new parameter has been added to the [`BillingClient.queryPurchasesAsync()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#queryPurchasesAsync(com.android.billingclient.api.QueryPurchasesParams,com.android.billingclient.api.PurchasesResponseListener)) method to include suspended subscriptions when querying for subscriptions. Suspended subscriptions are still attributed to the user, but are not active, either because the user paused the subscription or their renewal payment method was declined. + A new parameter has been added to the + [`BillingClient.queryPurchasesAsync()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#queryPurchasesAsync(com.android.billingclient.api.QueryPurchasesParams,com.android.billingclient.api.PurchasesResponseListener)) + method to include suspended subscriptions when querying for subscriptions. + Suspended subscriptions are still attributed to the user, but are not active, + either because the user paused the subscription or their renewal payment method + was declined. - The [`Purchase`](https://developer.android.com/reference/com/android/billingclient/api/Purchase) object returned in the listener will return `isSuspended() = true` for any suspended subscriptions. In this case, you shouldn't grant access to the purchased subscription, and instead guide the user to the [subscriptions center](https://play.google.com/store/account/subscriptions) where the user can manage their payment methods or pause state to re-activate their subscription. + The [`Purchase`](https://developer.android.com/reference/com/android/billingclient/api/Purchase) object returned in the listener will return `isSuspended() = true` + for any suspended subscriptions. In this case, you shouldn't grant access + to the purchased subscription, and instead guide the user to the + [subscriptions center](https://play.google.com/store/account/subscriptions) + where the user can manage their payment methods + or pause state to re-activate their subscription. - Updates to [subscriptions](https://developer.android.com/google/play/billing/subscriptions): - - The [`BillingFlowParams.ProductDetailsParams`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.ProductDetailsParams) object now has the `setSubscriptionProductReplacementParams()` method in which you can specify product level replacement information. + - The [`BillingFlowParams.ProductDetailsParams`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.ProductDetailsParams) + object now has the `setSubscriptionProductReplacementParams()` method + in which you can specify product level replacement information. - - The `SubscriptionProductReplacementParams` object has two setter methods: + - The `SubscriptionProductReplacementParams` object has + two setter methods: - `setOldProductId`: The old product that needs to be replaced by the product in current [`ProductDetails`](https://developer.android.com/reference/com/android/billingclient/api/ProductDetails). - `setReplacementMode`: This is the item level replacement mode. The modes are essentially the same as SubscriptionUpdateParams, but the value mapping has been updated. A new replacement mode `KEEP_EXISTING` is introduced that lets you keep the existing payment schedule unchanged for an item. - - [SubscriptionUpdateParams setSubscriptionReplacementMode](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.SubscriptionUpdateParams.Builder#setSubscriptionReplacementMode(int)) will be deprecated. You should use `SubscriptionProductReplacementParams.setReplacementMode` instead. + - [SubscriptionUpdateParams setSubscriptionReplacementMode](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.SubscriptionUpdateParams.Builder#setSubscriptionReplacementMode(int)) will be + deprecated. You should use `SubscriptionProductReplacementParams.setReplacementMode` + instead. - Updated `minSdkVersion` to 23. - Enabled [pre-order APIs for one-time products](https://developer.android.com/google/play/billing/one-time-product-multi-purchase-options-offers#pre-order) - The `ProductDetails.oneTimePurchaseOfferDetails.getPreorderDetails()` API that gets the pre-order details is now available for use. + The `ProductDetails.oneTimePurchaseOfferDetails.getPreorderDetails()` API + that gets the pre-order details is now available for use. - Google Play Billing Library now supports [Kotlin version 2.2.0](https://kotlinlang.org/docs/whatsnew22.html). ## Google Play Billing Library 8.0.0 Release (2025-06-30) -Version 8.0.0 of the Google Play Billing Library and Kotlin extensions are now available. +Version 8.0.0 of the Google Play Billing Library and Kotlin extensions are now +available. ### Summary of changes @@ -122,87 +170,137 @@ Version 8.0.0 of the Google Play Billing Library and Kotlin extensions are now a - Multiple purchase options and offers for one-time products. - You can now have multiple purchase options and offers for your one-time products. This provides you flexibility in how you sell your products and reduces the complexity of managing them. + You can now have multiple purchase options and offers for your one-time + products. This provides you flexibility in how you sell your products and + reduces the complexity of managing them. - Improved the [`queryProductDetailsAsync()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#queryProductDetailsAsync(com.android.billingclient.api.QueryProductDetailsParams,com.android.billingclient.api.ProductDetailsResponseListener)) method. - Prior to PBL 8.0.0, the [`queryProductDetailsAsync()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#queryProductDetailsAsync(com.android.billingclient.api.QueryProductDetailsParams,com.android.billingclient.api.ProductDetailsResponseListener)) method didn't return products that couldn't be fetched. This could be due to reasons such as the product is not found or no offers are available to the user. With PBL 8.0.0, unfetched products are returned with a new product-level status code that provides information about unfetched products. Note that there is a change in the signature of the [`ProductDetailsResponseListener.onProductDetailsResponse()`](https://developer.android.com/google/play/billing/integrate#automatic-service-reconnection) which requires changes in your app. For more information, see [process the result](https://developer.android.com/google/play/billing/integrate#process-the-result). + Prior to PBL 8.0.0, the [`queryProductDetailsAsync()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#queryProductDetailsAsync(com.android.billingclient.api.QueryProductDetailsParams,com.android.billingclient.api.ProductDetailsResponseListener)) method didn't + return products that couldn't be fetched. This could be due to reasons such + as the product is not found or no offers are available to the user. With PBL + 8.0.0, unfetched products are returned with a new product-level status code + that provides information about unfetched products. Note that there is a + change in the signature of the + [`ProductDetailsResponseListener.onProductDetailsResponse()`](https://developer.android.com/google/play/billing/integrate#automatic-service-reconnection) which + requires changes in your app. For more information, see [process the + result](https://developer.android.com/google/play/billing/integrate#process-the-result). - Automatic service reconnection. - The new `BillingClient.Builder.enableAutoServiceReconnection()` builder parameter lets developers opt-in to automatic service reconnection, which simplifies connection management by handling reconnections to the Play Billing Service automatically and eliminating the need to manually call `startConnection()` in the event of a service disconnection. For more information, see [Automatically Re-establish a Connection](https://developer.android.com/google/play/billing/integrate#automatic-service-reconnection). + The new `BillingClient.Builder.enableAutoServiceReconnection()` builder + parameter lets developers opt-in to automatic service reconnection, which + simplifies connection management by handling reconnections to + the Play Billing Service automatically and eliminating the need to manually + call `startConnection()` in the event of a service disconnection. + For more information, see [Automatically Re-establish a Connection](https://developer.android.com/google/play/billing/integrate#automatic-service-reconnection). - Sub-response codes for the [`launchBillingFlow()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#launchBillingFlow(android.app.Activity,com.android.billingclient.api.BillingFlowParams)) method. - The [BillingResult](https://developer.android.com/reference/com/android/billingclient/api/BillingResult) returned from [`launchBillingFlow()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#launchBillingFlow(android.app.Activity,com.android.billingclient.api.BillingFlowParams)) will now include a sub-response code field. This field will only be populated in some cases to provide a more specific reason for the failure. The sub-response field can have the following values: + The [BillingResult](https://developer.android.com/reference/com/android/billingclient/api/BillingResult) returned from [`launchBillingFlow()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#launchBillingFlow(android.app.Activity,com.android.billingclient.api.BillingFlowParams)) will now + include a sub-response code field. This field will only be populated in some + cases to provide a more specific reason for the failure. The sub-response + field can have the following values: - `PAYMENT_DECLINED_DUE_TO_INSUFFICIENT_FUNDS` - Returned when the user's funds are less than the price of the item they are attempting to purchase. - `USER_INELIGIBLE` - Returned when the user doesn't meet the configured eligibility requirements for a subscription offer. - `NO_APPLICABLE_SUB_RESPONSE_CODE` - The default value, returned when no other sub-response code is applicable. - Removed the `queryPurchaseHistory()` method. - The `queryPurchaseHistory()` method that was previously marked as deprecated has now been removed. See [Query Purchase History](https://developer.android.com/google/play/billing/query-purchase-history) for details on what alternative APIs to use instead. + The `queryPurchaseHistory()` method that was previously marked as + deprecated has now been removed. See [Query Purchase History](https://developer.android.com/google/play/billing/query-purchase-history) for + details on what alternative APIs to use instead. - Removed the `querySkuDetailsAsync()` method. - The `querySkuDetailsAsync()` method that was previously marked as deprecated has now been removed. You should use [queryProductDetailsAsync](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#queryProductDetailsAsync(com.android.billingclient.api.QueryProductDetailsParams,com.android.billingclient.api.ProductDetailsResponseListener)) instead. + The `querySkuDetailsAsync()` method that was previously marked as deprecated + has now been removed. You should use [queryProductDetailsAsync](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#queryProductDetailsAsync(com.android.billingclient.api.QueryProductDetailsParams,com.android.billingclient.api.ProductDetailsResponseListener)) instead. - Removed the `BillingClient.Builder.enablePendingPurchases()` method. - The `enablePendingPurchases()` method with no parameters that was previously marked as deprecated has now been removed. You should use `enablePendingPurchases(PendingPurchaseParams params)` instead. Note that the deprecated `enablePendingPurchases()` is functionally equivalent to `enablePendingPurchases(PendingPurchasesParams.newBuilder().enableOneTimeProducts().build())`. + The `enablePendingPurchases()` method with no parameters that was previously + marked as deprecated has now been removed. You should use + `enablePendingPurchases(PendingPurchaseParams params)` instead. Note that + the deprecated `enablePendingPurchases()` is functionally equivalent to + `enablePendingPurchases(PendingPurchasesParams.newBuilder().enableOneTimeProducts().build())`. - Removed the overloaded `queryPurchasesAsync()` method that takes a [skuType](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.SkuType). - The `queryPurchasesAsync(String skuType, PurchasesResponseListener listener)` method that was previously marked as deprecated has now been removed. Alternately, use [`queryPurchasesAsync(QueryPurchasesParams queryPurchasesParams, PurchasesResponseListener listener)`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#queryPurchasesAsync(com.android.billingclient.api.QueryPurchasesParams,com.android.billingclient.api.PurchasesResponseListener)). + The `queryPurchasesAsync(String skuType, PurchasesResponseListener + listener)` method that was previously marked as + deprecated has now been removed. Alternately, use + [`queryPurchasesAsync(QueryPurchasesParams queryPurchasesParams, + PurchasesResponseListener listener)`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#queryPurchasesAsync(com.android.billingclient.api.QueryPurchasesParams,com.android.billingclient.api.PurchasesResponseListener)). ## Google Play Billing Library 7.1.1 Release (2024-10-03) -Version 7.1.1 of the Google Play Billing Library and Kotlin extensions are now available. +Version 7.1.1 of the Google Play Billing Library and Kotlin extensions are now +available. ### Bug fixes -- Fixed a bug in Play Billing Library 7.1.0 related to [testing `BillingResult` response codes](https://developer.android.com/google/play/billing/test-response-codes). +- Fixed a bug in Play Billing Library 7.1.0 related to [testing + `BillingResult` response codes](https://developer.android.com/google/play/billing/test-response-codes). ## Google Play Billing Library 7.1.0 Release (2024-09-19) -Version 7.1.0 of the Google Play Billing Library and Kotlin extensions are now available. +Version 7.1.0 of the Google Play Billing Library and Kotlin extensions are now +available. ### Summary of changes - Improved thread safety related to connection status and management. -- Introduced partial changes for testing [`BillingResult`](https://developer.android.com/reference/com/android/billingclient/api/BillingResult) response codes which is fully released in Play Billing Library 7.1.1. To test your integration using this feature, you'll need to upgrade to Play Billing Library 7.1.1. A bug exists that will only impact applications with [billing overrides testing enabled](https://developer.android.com/google/play/billing/test-response-codes#enable-billing-overrides-testing) and does not affect regular usage. For more information, see [Test `BillingResult` response codes](https://developer.android.com/google/play/billing/test-response-codes). +- Introduced partial changes for testing [`BillingResult`](https://developer.android.com/reference/com/android/billingclient/api/BillingResult) response codes which is fully released in Play Billing Library 7.1.1. To test your integration using this feature, you'll need to upgrade to Play Billing Library 7.1.1. A bug exists that will only impact applications with [billing overrides testing enabled](https://developer.android.com/google/play/billing/test-response-codes#enable-billing-overrides-testing) and does not affect regular usage. For more information, see [Test `BillingResult` + response codes](https://developer.android.com/google/play/billing/test-response-codes). ## Google Play Billing Library 7.0.0 Release (2024-05-14) -Version 7.0.0 of the Google Play Billing Library and Kotlin extensions are now available. +Version 7.0.0 of the Google Play Billing Library and Kotlin extensions are now +available. ### Summary of changes - Added APIs to support installment subscriptions. - Added [`ProductDetails.InstallmentPlanDetails`](https://developer.android.com/reference/com/android/billingclient/api/ProductDetails.InstallmentPlanDetails) for installment base plans that users are eligible to purchase. This API helps your app identify the installment plan and its commitment setup to provide related information to the user. To learn more, see our [subscription installments guide](https://developer.android.com/google/play/billing/subscriptions#installments). -- Added [`PendingPurchasesParams`](https://developer.android.com/reference/com/android/billingclient/api/PendingPurchasesParams) and [`BillingClient.Builder.enablePendingPurchases(PendingPurchaseParams)`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.Builder#enablePendingPurchases(PendingPurchaseParams)) to replace [`BillingClient.Builder.enablePendingPurchases()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.Builder#enablePendingPurchases()), which has been deprecated in this release. +- Added [`PendingPurchasesParams`](https://developer.android.com/reference/com/android/billingclient/api/PendingPurchasesParams) and + [`BillingClient.Builder.enablePendingPurchases(PendingPurchaseParams)`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.Builder#enablePendingPurchases(PendingPurchaseParams)) + to replace + [`BillingClient.Builder.enablePendingPurchases()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.Builder#enablePendingPurchases()), + which has been deprecated in this release. - The deprecated `enablePendingPurchases()` is functionally equivalent to `enablePendingPurchases(PendingPurchasesParams.newBuilder().enableOneTimeProducts().build())`. - Added APIs to support pending transactions for subscription prepaid plans: - - Use [`PendingPurchasesParams.Builder.enablePrepaidPlans()`](https://developer.android.com/reference/com/android/billingclient/api/PendingPurchasesParams.Builder#enablePrepaidPlans()) along with [`BillingClient.Builder.enablePendingPurchases(PendingPurchaseParams)`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.Builder#enablePendingPurchases(PendingPurchaseParams)) to enable pending transactions for subscription prepaid plans. When adding support, be sure that your app also correctly manages subscription lifecycles. To learn more see our [pending purchases guide](https://developer.android.com/google/play/billing/subscriptions#pending). + - Use [`PendingPurchasesParams.Builder.enablePrepaidPlans()`](https://developer.android.com/reference/com/android/billingclient/api/PendingPurchasesParams.Builder#enablePrepaidPlans()) along with [`BillingClient.Builder.enablePendingPurchases(PendingPurchaseParams)`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.Builder#enablePendingPurchases(PendingPurchaseParams)) to enable pending transactions for subscription prepaid plans. When adding support, be sure that your app also correctly manages subscription lifecycles. To learn more see our [pending purchases + guide](https://developer.android.com/google/play/billing/subscriptions#pending). - Added [`Purchase.PendingPurchaseUpdate`](https://developer.android.com/reference/com/android/billingclient/api/Purchase.PendingPurchaseUpdate) and [`Purchase.getPendingPurchaseUpdate()`](https://developer.android.com/reference/com/android/billingclient/api/Purchase.getPendingPurchaseUpdate()) for retrieving the pending top-up or upgrade or downgrade to an existing subscription. -- Removed [`BillingClient.Builder.enableAlternativeBilling()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.Builder#enableAlternativeBilling(com.android.billingclient.api.AlternativeBillingListener)), [`AlternativeBillingListener`](https://developer.android.com/reference/com/android/billingclient/api/AlternativeBillingListener), and [`AlternativeChoiceDetails`](https://developer.android.com/reference/com/android/billingclient/api/AlternativeChoiceDetails). +- Removed + [`BillingClient.Builder.enableAlternativeBilling()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.Builder#enableAlternativeBilling(com.android.billingclient.api.AlternativeBillingListener)), + [`AlternativeBillingListener`](https://developer.android.com/reference/com/android/billingclient/api/AlternativeBillingListener), and + [`AlternativeChoiceDetails`](https://developer.android.com/reference/com/android/billingclient/api/AlternativeChoiceDetails). - Developers should use [`BillingClient.Builder.enableUserChoiceBilling()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.Builder#enableUserChoiceBilling(com.android.billingclient.api.UserChoiceBillingListener)) with [`UserChoiceBillingListener`](https://developer.android.com/reference/com/android/billingclient/api/UserChoiceBillingListener) and [`UserChoiceDetails`](https://developer.android.com/reference/com/android/billingclient/api/UserChoiceDetails) in the listener callback instead. -- Removed [`BillingFlowParams.ProrationMode`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.ProrationMode), [`BillingFlowParams.SubscriptionUpdateParams.Builder.setReplaceProrationMode()`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.SubscriptionUpdateParams.Builder#setReplaceProrationMode(int)), and [`BillingFlowParams.SubscriptionUpdateParams.Builder.setReplaceSkusProrationMode()`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.SubscriptionUpdateParams.Builder#setReplaceSkusProrationMode(int)). +- Removed [`BillingFlowParams.ProrationMode`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.ProrationMode), + [`BillingFlowParams.SubscriptionUpdateParams.Builder.setReplaceProrationMode()`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.SubscriptionUpdateParams.Builder#setReplaceProrationMode(int)), + and + [`BillingFlowParams.SubscriptionUpdateParams.Builder.setReplaceSkusProrationMode()`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.SubscriptionUpdateParams.Builder#setReplaceSkusProrationMode(int)). - Developers should use [`BillingFlowParams.SubscriptionUpdateParams.ReplacementMode`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.SubscriptionUpdateParams.ReplacementMode) with [`BillingFlowParams.SubscriptionUpdateParams.Builder#setSubscriptionReplacementMode(int)`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.SubscriptionUpdateParams.Builder#setSubscriptionReplacementMode(int)) instead. -- Removed [`BillingFlowParams.SubscriptionUpdateParams.Builder#setOldSkuPurchaseToken()`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.SubscriptionUpdateParams.Builder#setOldSkuPurchaseToken(java.lang.String)). +- Removed + [`BillingFlowParams.SubscriptionUpdateParams.Builder#setOldSkuPurchaseToken()`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.SubscriptionUpdateParams.Builder#setOldSkuPurchaseToken(java.lang.String)). - Developers should use [`BillingFlowParams.SubscriptionUpdateParams.Builder#setOldPurchaseToken(java.lang.String)`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.SubscriptionUpdateParams.Builder#setOldPurchaseToken(java.lang.String)) instead. -- [`BillingClient.queryPurchaseHistoryAsync()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#queryPurchaseHistoryAsync(com.android.billingclient.api.QueryPurchaseHistoryParams,com.android.billingclient.api.PurchaseHistoryResponseListener)) has been deprecated and will be removed in a future release. Developers should use the following alternatives instead: +- [`BillingClient.queryPurchaseHistoryAsync()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#queryPurchaseHistoryAsync(com.android.billingclient.api.QueryPurchaseHistoryParams,com.android.billingclient.api.PurchaseHistoryResponseListener)) + has been deprecated and will be removed in a future release. Developers + should use the following alternatives instead: - Acknowledged and pending purchases: Use [`BillingClient.queryPurchasesAsync()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#queryPurchasesAsync(com.android.billingclient.api.QueryPurchasesParams,%20com.android.billingclient.api.PurchasesResponseListener)) to fetch the active purchases. - Consumed purchases: Developers should keep track of consumed purchases on their own servers. - Canceled purchases: Use the [voided-purchases](https://developers.google.com/android-publisher/voided-purchases) developer API. - For more details, see [Query Purchase History](https://developer.android.com/google/play/billing/query-purchase-history) -- [`BillingFlowParams.ProductDetailsParams.setOfferToken()`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.ProductDetailsParams.Builder#setOfferToken(java.lang.String)) now throws an exception when developers specify an empty `offerToken`. +- [`BillingFlowParams.ProductDetailsParams.setOfferToken()`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.ProductDetailsParams.Builder#setOfferToken(java.lang.String)) + now throws an exception when developers specify an empty `offerToken`. - Updated `minSdkVersion` to 21 and `targetSdkVersion` to 34. ## Google Play Billing Library 6.2.1 Release (2024-04-16) -Version 6.2.1 of the Google Play Billing Library and Kotlin extensions are now available. +Version 6.2.1 of the Google Play Billing Library and Kotlin extensions are now +available. ### Summary of changes @@ -210,7 +308,8 @@ Version 6.2.1 of the Google Play Billing Library and Kotlin extensions are now a ## Google Play Billing Library 6.2.0 Release (2024-03-06) -Version 6.2.0 of the Google Play Billing Library and Kotlin extensions are now available. +Version 6.2.0 of the Google Play Billing Library and Kotlin extensions are now +available. ### Summary of changes @@ -222,7 +321,8 @@ Version 6.2.0 of the Google Play Billing Library and Kotlin extensions are now a ## Google Play Billing Library 6.1.0 Release (2023-11-14) -Version 6.1.0 of the Google Play Billing Library and Kotlin extensions are now available. +Version 6.1.0 of the Google Play Billing Library and Kotlin extensions are now +available. ### Summary of changes @@ -239,7 +339,8 @@ Version 6.1.0 of the Google Play Billing Library and Kotlin extensions are now a ## Google Play Billing Library 6.0.1 Release (2023-06-22) -Version 6.0.1 of the Google Play Billing Library and Kotlin extensions are now available. +Version 6.0.1 of the Google Play Billing Library and Kotlin extensions are now +available. ### Summary of changes @@ -247,37 +348,65 @@ Update Play Billing Library to be compatible with Android 14. ## Google Play Billing Library 6.0 Release (2023-05-10) -Version 6.0.0 of the Google Play Billing Library and Kotlin extensions are now available. +Version 6.0.0 of the Google Play Billing Library and Kotlin extensions are now +available. ### Summary of changes -- Added new [`ReplacementMode`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.SubscriptionUpdateParams.ReplacementMode) enum to replace [`ProrationMode`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.ProrationMode). +- Added new [`ReplacementMode`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.SubscriptionUpdateParams.ReplacementMode) + enum to replace + [`ProrationMode`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.ProrationMode). - Note that `ProrationMode` is still available for backward compatibility reasons. -- Removed order ID for [`PENDING`](https://developer.android.com/reference/com/android/billingclient/api/Purchase.PurchaseState#PENDING) purchases. + Note that `ProrationMode` is still available for backward compatibility + reasons. +- Removed order ID for [`PENDING`](https://developer.android.com/reference/com/android/billingclient/api/Purchase.PurchaseState#PENDING) + purchases. - Previously, the order ID would always be created even if the purchase was pending. Starting with version 6.0.0, an order ID will not be created for pending purchases, and for these purchases, the order ID will be populated after the purchase is moved to the [`PURCHASED`](https://developer.android.com/reference/com/android/billingclient/api/Purchase.PurchaseState#PURCHASED) state. + Previously, the order ID would always be created even if the purchase was + pending. Starting with version 6.0.0, an order ID will not be created for + pending purchases, and for these purchases, the order ID will be populated + after the purchase is moved to the + [`PURCHASED`](https://developer.android.com/reference/com/android/billingclient/api/Purchase.PurchaseState#PURCHASED) + state. - Removed `queryPurchases` and `launchPriceConfirmationFlow` methods. - The `queryPurchases` and `launchPriceConfirmationFlow` methods that have previously been marked as deprecated have now been removed in Play Billing Library 6.0.0. Developers should use [`queryPurchasesAsync`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#queryPurchasesAsync(com.android.billingclient.api.QueryPurchasesParams,%20com.android.billingclient.api.PurchasesResponseListener)) instead of `queryPurchases`. For `launchPriceConfirmationFlow` alternatives, see [Price changes](https://developer.android.com/google/play/billing/price-changes). + The `queryPurchases` and `launchPriceConfirmationFlow` methods that have + previously been marked as deprecated have now been removed in Play Billing + Library 6.0.0. Developers should use + [`queryPurchasesAsync`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#queryPurchasesAsync(com.android.billingclient.api.QueryPurchasesParams,%20com.android.billingclient.api.PurchasesResponseListener)) + instead of `queryPurchases`. For `launchPriceConfirmationFlow` alternatives, + see [Price changes](https://developer.android.com/google/play/billing/price-changes). - Added new network error response code. - A new network error response code, [`NETWORK_ERROR`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.BillingResponseCode#NETWORK_ERROR), has been added starting with PBL version 6.0.0. This code is returned when an error occurs due to a network connection issue. These network connection errors were previously reported as `SERVICE_UNAVAILABLE`. -- Updated [`SERVICE_UNAVAILABLE`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.BillingResponseCode#SERVICE_UNAVAILABLE) and [`SERVICE_TIMEOUT`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.BillingResponseCode#SERVICE_TIMEOUT). + A new network error response code, + [`NETWORK_ERROR`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.BillingResponseCode#NETWORK_ERROR), + has been added starting with PBL version 6.0.0. This code is returned when + an error occurs due to a network connection issue. These network connection + errors were previously reported as `SERVICE_UNAVAILABLE`. +- Updated [`SERVICE_UNAVAILABLE`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.BillingResponseCode#SERVICE_UNAVAILABLE) + and + [`SERVICE_TIMEOUT`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.BillingResponseCode#SERVICE_TIMEOUT). - Starting with PBL version 6.0.0, errors due to timeout in processing will be returned as `SERVICE_UNAVAILABLE` instead of the current `SERVICE_TIMEOUT`. + Starting with PBL version 6.0.0, errors due to timeout in processing will be + returned as `SERVICE_UNAVAILABLE` instead of the current `SERVICE_TIMEOUT`. The behavior does not change in earlier versions of PBL. -- Removed [`SERVICE_TIMEOUT`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.BillingResponseCode#SERVICE_TIMEOUT). +- Removed + [`SERVICE_TIMEOUT`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.BillingResponseCode#SERVICE_TIMEOUT). - Starting with PBL version 6.0.0, `SERVICE_TIMEOUT` will no longer be returned. Previous versions of PBL will still return this code. + Starting with PBL version 6.0.0, `SERVICE_TIMEOUT` will no longer be + returned. Previous versions of PBL will still return this code. - Added additional logging. - The Play Billing Library 6 release includes additional logging, which provides insight into API usage (such as success and failure) and service connection issues. This information will be used to improve the performance of the Play Billing Library and provide better support for errors. + The Play Billing Library 6 release includes additional logging, which + provides insight into API usage (such as success and failure) and service + connection issues. This information will be used to improve the performance + of the Play Billing Library and provide better support for errors. ## Google Play Billing Library 5.2.1 Release (2023-06-22) -Version 5.2.1 of the Google Play Billing Library and Kotlin extensions are now available. +Version 5.2.1 of the Google Play Billing Library and Kotlin extensions are now +available. ### Summary of changes @@ -285,7 +414,8 @@ Update Play Billing Library to be compatible with Android 14. ## Google Play Billing Library 5.2 Release (2023-04-06) -Version 5.2.0 of the Google Play Billing Library and Kotlin extensions are now available. +Version 5.2.0 of the Google Play Billing Library and Kotlin extensions are now +available. ### Summary of changes @@ -298,7 +428,8 @@ Version 5.2.0 of the Google Play Billing Library and Kotlin extensions are now a ## Google Play Billing Library 5.1 Release (2022-10-31) -Version 5.1.0 of the Google Play Billing Library and Kotlin extensions are now available. +Version 5.1.0 of the Google Play Billing Library and Kotlin extensions are now +available. This version contains the following changes. @@ -310,7 +441,8 @@ This version contains the following changes. ## Google Play Billing Library 5.0 Release (2022-05-11) -Version 5.0.0 of the Google Play Billing Library and Kotlin extensions are now available. +Version 5.0.0 of the Google Play Billing Library and Kotlin extensions are now +available. This version contains the following changes. @@ -318,46 +450,75 @@ This version contains the following changes. - Introduced a new model for subscriptions, including new entities that enable you to create multiple offers for a single subscription product. For more information, see the [migration guide](https://developer.android.com/google/play/billing/migrate-gpblv5). - Added [`BillingClient.queryProductDetailsAsync()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#queryProductDetailsAsync(com.android.billingclient.api.QueryProductDetailsParams,%20com.android.billingclient.api.ProductDetailsResponseListener)) to replace `BillingClient.querySkuDetailsAsync()`. -- Added `setIsOfferPersonalized()` method for EU personalized pricing disclosure requirements. To learn more about how to use this method, see [Indicate a personalized price](https://developer.android.com/google/play/billing/integrate#personalized-price). +- Added `setIsOfferPersonalized()` method for EU personalized pricing disclosure requirements. To learn more about how to use this method, see [Indicate a personalized + price](https://developer.android.com/google/play/billing/integrate#personalized-price). - Removed `queryPurchases()`, which was previously deprecated and replaced by queryPurchasesAsync introduced in Google Play Billing Library 4.0.0. -- `launchPriceChangeFlow` has been deprecated and will be removed in a future release. To learn more about alternatives, see [Launch a price change confirmation flow](https://developer.android.com/google/play/billing/subscriptions#price-change-launch). +- `launchPriceChangeFlow` has been deprecated and will be removed in a future release. To learn more about alternatives, see [Launch a price change + confirmation flow](https://developer.android.com/google/play/billing/subscriptions#price-change-launch). - Removed [`setVrPurchaseFlow()`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.Builder#setVrPurchaseFlow(boolean)), which was previously used when instantiating a purchase flow. In previous versions, this method redirected the user to complete the purchase on their Android-powered device. Once you remove this method, users will complete the purchase through the standard purchase flow. ## Google Play Billing Library 4.1 release (2022-02-23) -Version 4.1.0 of the Google Play Billing Library and Kotlin extensions are now available. +Version 4.1.0 of the Google Play Billing Library and Kotlin extensions are now +available. This version contains the following changes. ### Summary of changes -- Added [`BillingClient.showInAppMessages()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#showInAppMessages(android.app.Activity,%20com.android.billingclient.api.InAppMessageParams,%20com.android.billingclient.api.InAppMessageResponseListener)) to help with handling subscription payment declines. To learn more about how to use in-app messaging for handling subscriptions payment declines, see [Handling payment declines](https://developer.android.com/google/play/billing/subscriptions#payment-declines). +- Added [`BillingClient.showInAppMessages()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#showInAppMessages(android.app.Activity,%20com.android.billingclient.api.InAppMessageParams,%20com.android.billingclient.api.InAppMessageResponseListener)) to help with handling subscription payment declines. To learn more about how to use in-app messaging for handling subscriptions payment declines, see [Handling payment + declines](https://developer.android.com/google/play/billing/subscriptions#payment-declines). ## Google Play Billing Library 4.0 Release (2021-05-18) -Version 4.0.0 of the Google Play Billing Library and Kotlin extensions are now available. +Version 4.0.0 of the Google Play Billing Library and Kotlin extensions are now +available. ### Summary of changes -- Added [`BillingClient.queryPurchasesAsync()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#queryPurchasesAsync(java.lang.String,%20com.android.billingclient.api.PurchasesResponseListener)) to replace [`BillingClient.queryPurchases()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#queryPurchases(java.lang.String)) which will be removed in a future release. +- Added + [`BillingClient.queryPurchasesAsync()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#queryPurchasesAsync(java.lang.String,%20com.android.billingclient.api.PurchasesResponseListener)) + to replace + [`BillingClient.queryPurchases()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#queryPurchases(java.lang.String)) + which will be removed in a future release. -- Added new subscription replacement mode [`IMMEDIATE_AND_CHARGE_FULL_PRICE`](https://developer.android.com/google/play/billing/subscriptions#change). +- Added new subscription replacement mode + [`IMMEDIATE_AND_CHARGE_FULL_PRICE`](https://developer.android.com/google/play/billing/subscriptions#change). -- Added [`BillingClient.getConnectionState()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#getConnectionState()) method to retrieve the Play Billing Library's connection state. +- Added + [`BillingClient.getConnectionState()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#getConnectionState()) + method to retrieve the Play Billing Library's connection state. -- Updated Javadoc and implementation to indicate which thread a method can be called on and which thread results are posted. +- Updated Javadoc and implementation to indicate which thread a method can be + called on and which thread results are posted. -- Added [`BillingFlowParams.Builder.setSubscriptionUpdateParams()`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.Builder#setSubscriptionUpdateParams(com.android.billingclient.api.BillingFlowParams.SubscriptionUpdateParams)) as a new way to initiate subscription updates. This replaces `BillingFlowParams#getReplaceSkusProrationMode`, `BillingFlowParams#getOldSkuPurchaseToken`, `BillingFlowParams#getOldSku`, `BillingFlowParams.Builder#setReplaceSkusProrationMode`, `BillingFlowParams.Builder#setOldSku` which have been removed. +- Added + [`BillingFlowParams.Builder.setSubscriptionUpdateParams()`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.Builder#setSubscriptionUpdateParams(com.android.billingclient.api.BillingFlowParams.SubscriptionUpdateParams)) + as a new way to initiate subscription updates. This replaces + `BillingFlowParams#getReplaceSkusProrationMode`, + `BillingFlowParams#getOldSkuPurchaseToken`, `BillingFlowParams#getOldSku`, + `BillingFlowParams.Builder#setReplaceSkusProrationMode`, + `BillingFlowParams.Builder#setOldSku` which have been removed. -- Added [`Purchase.getQuantity()`](https://developer.android.com/reference/com/android/billingclient/api/Purchase#getQuantity()) and [`PurchaseHistoryRecord.getQuantity()`](https://developer.android.com/reference/com/android/billingclient/api/PurchaseHistoryRecord#getQuantity()). +- Added + [`Purchase.getQuantity()`](https://developer.android.com/reference/com/android/billingclient/api/Purchase#getQuantity()) + and + [`PurchaseHistoryRecord.getQuantity()`](https://developer.android.com/reference/com/android/billingclient/api/PurchaseHistoryRecord#getQuantity()). -- Added [`Purchase#getSkus()`](https://developer.android.com/reference/com/android/billingclient/api/Purchase#getSkus()) and [`PurchaseHistoryRecord#getSkus()`](https://developer.android.com/reference/com/android/billingclient/api/PurchaseHistoryRecord#getSkus()). These replace `Purchase#getSku` and `PurchaseHistoryRecord#getSku` which have been removed. +- Added + [`Purchase#getSkus()`](https://developer.android.com/reference/com/android/billingclient/api/Purchase#getSkus()) + and + [`PurchaseHistoryRecord#getSkus()`](https://developer.android.com/reference/com/android/billingclient/api/PurchaseHistoryRecord#getSkus()). + These replace `Purchase#getSku` and `PurchaseHistoryRecord#getSku` which + have been removed. -- Removed `BillingFlowParams#getSku`, `BillingFlowParams#getSkuDetails` and `BillingFlowParams#getSkuType`. +- Removed `BillingFlowParams#getSku`, `BillingFlowParams#getSkuDetails` and + `BillingFlowParams#getSkuType`. ## Google Play Billing Library 3.0.3 Release (2021-03-12) -Version 3.0.3 of the Google Play Billing Library, Kotlin extension, and Unity plugin are now available. +Version 3.0.3 of the Google Play Billing Library, Kotlin extension, and Unity +plugin are now available. ### Java and Kotlin Bug fixes @@ -370,7 +531,8 @@ Version 3.0.3 of the Google Play Billing Library, Kotlin extension, and Unity pl ## Google Play Billing Library 3.0.2 Release (2020-11-24) -Version 3.0.2 of the Google Play Billing Library and Kotlin extension are now available. +Version 3.0.2 of the Google Play Billing Library and Kotlin extension are now +available. ### Bug fixes @@ -379,7 +541,8 @@ Version 3.0.2 of the Google Play Billing Library and Kotlin extension are now av ## Google Play Billing Library 3.0.1 Release (2020-09-30) -Version 3.0.1 of the Google Play Billing Library and Kotlin extension are now available. +Version 3.0.1 of the Google Play Billing Library and Kotlin extension are now +available. ### Bug fixes @@ -387,7 +550,8 @@ Version 3.0.1 of the Google Play Billing Library and Kotlin extension are now av ## Google Play Billing Library 3.0 Release (2020-06-08) -Version 3.0.0 of the Google Play Billing Library, Kotlin extension, and Unity plugin are now available. +Version 3.0.0 of the Google Play Billing Library, Kotlin extension, and Unity +plugin are now available. ### Summary of changes @@ -413,13 +577,23 @@ Version 2.2.1 of the Google Play Billing library is now available. ## Google Play Billing Library 2.2.0 release and Unity support (2020-03-23) -Version 2.2.0 of the Google Play Billing provides functionality that helps developers ensure purchases are attributed to the correct user. These changes replace the need to build custom solutions based on developer payload. As part of this update, the developer payload functionality has been deprecated and will be removed in a future release. For more information, including recommended alternatives, see [Developer payload](https://developer.android.com/google/play/billing/developer-payload). +Version 2.2.0 of the Google Play Billing provides functionality that helps +developers ensure purchases are attributed to the correct user. These changes +replace the need to build custom solutions based on developer payload. As part +of this update, the developer payload functionality has been deprecated and will +be removed in a future release. For more information, including recommended +alternatives, see [Developer payload](https://developer.android.com/google/play/billing/developer-payload). ### Google Play Billing Billing Library 2 for Unity -In addition to the current Java and Kotlin versions of Google Play Billing Library 2, we released a version of the library for use with Unity. Game developers using the Unity in-app purchase API can upgrade now to take advantage of all Google Play Billing Library 2 features and to make the subsequent upgrades to future versions of the Google Play Billing Library easier. +In addition to the current Java and Kotlin versions of Google Play Billing +Library 2, we released a version of the library for use with Unity. Game +developers using the Unity in-app purchase API can upgrade now to take advantage +of all Google Play Billing Library 2 features and to make the subsequent +upgrades to future versions of the Google Play Billing Library easier. -To learn more, see [Use Google Play Billing with Unity](https://developer.android.com/google/play/billing/unity). +To learn more, see [Use Google Play Billing with +Unity](https://developer.android.com/google/play/billing/unity). ### Summary of changes @@ -427,19 +601,27 @@ To learn more, see [Use Google Play Billing with Unity](https://developer.androi - In [`AcknowledgePurchaseParams`](https://developer.android.com/reference/com/android/billingclient/api/AcknowledgePurchaseParams), deprecated [`setDeveloperPayload()`](https://developer.android.com/reference/com/android/billingclient/api/AcknowledgePurchaseParams.Builder#setDeveloperPayload(java.lang.String)) and [`getDeveloperPayload()`](https://developer.android.com/reference/com/android/billingclient/api/AcknowledgePurchaseParams#getDeveloperPayload()) methods. - In [`ConsumeParams`](https://developer.android.com/reference/com/android/billingclient/api/ConsumeParams), deprecated [`setDeveloperPayload()`](https://developer.android.com/reference/com/android/billingclient/api/ConsumeParams.Builder#setDeveloperPayload(java.lang.String)) and [`getDeveloperPayload()`](https://developer.android.com/reference/com/android/billingclient/api/ConsumeParams#getdeveloperpayload) methods. - In [`BillingFlowParams`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.Builder), renamed [`setAccountId()`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.Builder#setAccountId(java.lang.String)) to [`setObfuscatedAccountId()`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.Builder#setObfuscatedAccountId(java.lang.String)), and documented length restriction of 64 characters and restriction disallowing Personally Identifiable Information (PII) in this field. [`setAccountId()`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.Builder#setaccountid) has been marked as deprecated and will be removed in a future library version. - - In `BillingFlowParams`, added [`setObfuscatedProfileId()`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.Builder#setobfuscatedprofileid) which works similarly to [`setObfuscatedAccountId()`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.Builder#setObfuscatedAccountId(java.lang.String)). For more information, see [Developer payload updates and alternatives](https://developer.android.com/google/play/billing/developer-payload). + - In `BillingFlowParams`, added [`setObfuscatedProfileId()`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.Builder#setobfuscatedprofileid) which works similarly to [`setObfuscatedAccountId()`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.Builder#setObfuscatedAccountId(java.lang.String)). For more information, see [Developer payload updates and + alternatives](https://developer.android.com/google/play/billing/developer-payload). - In [`Purchase`](https://developer.android.com/reference/com/android/billingclient/api/Purchase), added the [`getAccountIdentifiers()`](https://developer.android.com/reference/com/android/billingclient/api/Purchase#getAccountIdentifiers()) method to return the obfuscated account identifiers set in `BillingFlowParams`. - - In [`BillingClient`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient), the [`loadRewardedSku()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#loadRewardedSku(com.android.billingclient.api.RewardLoadParams,%20com.android.billingclient.api.RewardResponseListener)) method has been marked deprecated as part of deprecating rewarded SKUs. You can find more information about the deprecation in the [Play Console Help Center](https://support.google.com/googleplay/android-developer/answer/9155268). + - In [`BillingClient`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient), the [`loadRewardedSku()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#loadRewardedSku(com.android.billingclient.api.RewardLoadParams,%20com.android.billingclient.api.RewardResponseListener)) method has been marked deprecated as part of deprecating rewarded SKUs. You can find more information about the deprecation in the [Play Console + Help + Center](https://support.google.com/googleplay/android-developer/answer/9155268). ## Google Play Billing Library 2.1.0 Release and Kotlin Extension 2.1.0 Release (2019-12-10) -Version 2.1.0 of the Google Play Billing library and the new Kotlin extension are now available. The Play Billing Library Kotlin extension provides idiomatic API alternatives for Kotlin consumption, featuring better null-safety and coroutines. For code examples, see [Use the Google Play Billing Library](https://developer.android.com/google/play/billing/billing_library_overview). +Version 2.1.0 of the Google Play Billing library and the new Kotlin extension +are now available. The Play Billing Library Kotlin extension provides idiomatic +API alternatives for Kotlin consumption, featuring better null-safety and +coroutines. For code examples, see [Use the Google Play Billing +Library](https://developer.android.com/google/play/billing/billing_library_overview). This version contains the following changes. ### Summary of changes -- In [`BillingFlowParams`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams), deprecated `setOldSku(String oldSku)` and replaced with `setOldSku(String oldSku, String purchaseToken)`, to disambiguate when multiple accounts on the device own the same sku. +- In [`BillingFlowParams`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams), deprecated `setOldSku(String oldSku)` and replaced with `setOldSku(String + oldSku, String purchaseToken)`, to disambiguate when multiple accounts on the device own the same sku. ## Google Play Billing Library 2.0.3 Release (2019-08-05) @@ -451,11 +633,14 @@ Version 2.0.3 of the Google Play Billing library is now available. ## Google Play Billing Library 2.0.2 Release (2019-07-08) -Version 2.0.2 of the Google Play Billing library is now available. This release contains updates to the reference documentation and does not change library functionality. +Version 2.0.2 of the Google Play Billing library is now available. This release +contains updates to the reference documentation and does not change library +functionality. ## Google Play Billing Library 2.0.1 Release (2019-06-06) -Version 2.0.1 of the Google Play Billing library is now available. This version contains the following changes. +Version 2.0.1 of the Google Play Billing library is now available. This version +contains the following changes. ### Bug fixes @@ -464,7 +649,8 @@ Version 2.0.1 of the Google Play Billing library is now available. This version ## Google Play Billing Library 2.0 Release (2019-05-07) -Version 2.0 of the Google Play Billing library is now available. This version contains the following changes. +Version 2.0 of the Google Play Billing library is now available. This version +contains the following changes. ### Purchases must be acknowledged within three days @@ -474,11 +660,32 @@ Version 2.0 of the Google Play Billing library is now available. This version co > [!NOTE] > **Note:** This requirement applies only to apps that use the Google Play Billing Library version 2.0 and newer. This requirement doesn't apply if you use an older version of the Google Play Billing Library, or if you use the AIDL API. -Google Play supports purchasing products from inside of your app (in-app) or outside of your app (out-of-app). In order for Google Play to ensure a consistent purchase experience regardless of where the user purchases your product, you must acknowledge all purchases received through the Google Play Billing Library as soon as possible after granting entitlement to the user. If you don't acknowledge a purchase within three days, the user automatically receives a refund, and Google Play revokes the purchase. For [pending transactions](https://developer.android.com/google/play/billing/release-notes#2_0_pending) (new in version 2.0), the three-day window starts when the purchase has moved to the `PURCHASED` state and does not apply while the purchase is in a `PENDING` state. +Google Play supports purchasing products from inside of your app (in-app) or +outside of your app (out-of-app). In order for Google Play to ensure a +consistent purchase experience regardless of where the user purchases your +product, you must acknowledge all purchases received through the Google Play +Billing Library as soon as possible after granting entitlement to the user. If +you don't acknowledge a purchase within three days, the user automatically +receives a refund, and Google Play revokes the purchase. For [pending +transactions](https://developer.android.com/google/play/billing/release-notes#2_0_pending) (new in version 2.0), the three-day window starts +when the purchase has moved to the `PURCHASED` state and does not apply while +the purchase is in a `PENDING` state. -For subscriptions, you must acknowledge any purchase that has a new purchase token. This means that all initial purchases, plan changes, and re-signups need to be acknowledged, but you don't need to acknowledge subsequent renewals. To determine if a purchase needs acknowledgment, you can check the acknowledgement field in the purchase. +For subscriptions, you must acknowledge any purchase that has a new purchase +token. This means that all initial purchases, plan changes, and re-signups need +to be acknowledged, but you don't need to acknowledge subsequent renewals. To +determine if a purchase needs acknowledgment, you can check the acknowledgement +field in the purchase. -The `Purchase` object now includes an [`isAcknowledged()`](https://developer.android.com/reference/com/android/billingclient/api/Purchase#isacknowledged) method that indicates whether a purchase has been acknowledged. In addition, the Google Play Developer API includes acknowledgement boolean values for both [`Purchases.products`](https://developers.google.com/android-publisher/api-ref/purchases/products) and [`Purchases.subscriptions`](https://developers.google.com/android-publisher/api-ref/purchases/subscriptions). Before acknowledging a purchase, be sure to use these methods to determine if the purchase has already been acknowledged. +The `Purchase` object now includes an +[`isAcknowledged()`](https://developer.android.com/reference/com/android/billingclient/api/Purchase#isacknowledged) +method that indicates whether a purchase has been acknowledged. In addition, the +Google Play Developer API includes acknowledgement boolean values for both +[`Purchases.products`](https://developers.google.com/android-publisher/api-ref/purchases/products) +and +[`Purchases.subscriptions`](https://developers.google.com/android-publisher/api-ref/purchases/subscriptions). +Before acknowledging a purchase, be sure to use these methods to determine if +the purchase has already been acknowledged. You can acknowledge a purchase by using one of the following methods: @@ -488,18 +695,32 @@ You can acknowledge a purchase by using one of the following methods: ### BillingFlowParams.setSku() has been removed -The previously-deprecated `BillingFlowParams#setSku()` method has been removed in this release. Before rendering products in a purchase flow, you must now call [`BillingClient.querySkuDetailsAsync()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#queryskudetailsasync), passing the resulting [`SkuDetails`](https://developer.android.com/reference/com/android/billingclient/api/SkuDetails) object to [`BillingFlowParams.Builder.setSkuDetails()`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.Builder#setskudetails). +The previously-deprecated `BillingFlowParams#setSku()` method has been removed +in this release. Before rendering products in a purchase flow, you must now call +[`BillingClient.querySkuDetailsAsync()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#queryskudetailsasync), +passing the resulting +[`SkuDetails`](https://developer.android.com/reference/com/android/billingclient/api/SkuDetails) object to +[`BillingFlowParams.Builder.setSkuDetails()`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.Builder#setskudetails). > [!NOTE] > **Note:** Caching `SkuDetails` between user sessions is not recommended, as `SkuDetails` objects are valid only for a limited time before you must refresh them again by calling `querySkuDetailsAsync()`. -For code examples, see [Use the Google Play Billing Library](https://developer.android.com/google/play/billing/billing_library_overview). +For code examples, see [Use the Google Play Billing +Library](https://developer.android.com/google/play/billing/billing_library_overview). ### Developer payload is supported -Version 2.0 of the Google Play Billing library adds support for *developer payload* ---arbitrary strings that can be attached to purchases. You can attach a developer payload parameter to a purchase, but only when the purchase is acknowledged or consumed. This is unlike developer payload in AIDL, where the payload could be specified when launching the purchase flow. Because purchases can now be initiated [from outside of your app](https://developer.android.com/google/play/billing/release-notes#2_0_acknowledge), this change ensures that you always have an opportunity to add a payload to purchases. +Version 2.0 of the Google Play Billing library adds support for *developer +payload* ---arbitrary strings that can be attached to purchases. You can +attach a developer payload parameter to a purchase, but only when the purchase +is acknowledged or consumed. This is unlike developer payload in AIDL, where the +payload could be specified when launching the purchase flow. Because purchases +can now be initiated [from outside of your app](https://developer.android.com/google/play/billing/release-notes#2_0_acknowledge), this change +ensures that you always have an opportunity to add a payload to purchases. -To access the payload in the new library, `Purchase` objects now include a [`getDeveloperPayload()`](https://developer.android.com/reference/com/android/billingclient/api/Purchase#getdeveloperpayload) method. +To access the payload in the new library, `Purchase` objects now include a +[`getDeveloperPayload()`](https://developer.android.com/reference/com/android/billingclient/api/Purchase#getdeveloperpayload) +method. > [!NOTE] > **Note:** You cannot modify a payload after it is assigned. @@ -509,9 +730,11 @@ To access the payload in the new library, `Purchase` objects now include a [`get > [!NOTE] > **Note:** This feature is being tested, and general availability is not guaranteed. -When you offer a discounted SKU, Google Play now returns the original price of the SKU so that you can show users that they are receiving a discount. +When you offer a discounted SKU, Google Play now returns the original price of +the SKU so that you can show users that they are receiving a discount. -[`SkuDetails`](https://developer.android.com/reference/com/android/billingclient/api/SkuDetails) contains two new methods for retrieving the original SKU price: +[`SkuDetails`](https://developer.android.com/reference/com/android/billingclient/api/SkuDetails) contains two +new methods for retrieving the original SKU price: - [`getOriginalPriceAmountMicros()`](https://developer.android.com/reference/com/android/billingclient/api/SkuDetails#getOriginalPriceAmountMicros()) - returns the unformatted original price of the SKU before discount. @@ -520,42 +743,78 @@ When you offer a discounted SKU, Google Play now returns the original price of t ### Pending transactions -With version 2.0 of the Google Play Billing library, you *must* support purchases where additional action is required before granting entitlement. For example, a user might choose to purchase your in-app product at a physical store using cash. This means that the transaction is completed outside of your app. In this scenario, you should grant entitlement only after the user has completed the transaction. +With version 2.0 of the Google Play Billing library, you *must* support +purchases where additional action is required before granting entitlement. For +example, a user might choose to purchase your in-app product at a physical store +using cash. This means that the transaction is completed outside of your app. In +this scenario, you should grant entitlement only after the user has completed +the transaction. -To enable pending purchases, call [`enablePendingPurchases()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.Builder#enablependingpurchases) as part of initializing your app. +To enable pending purchases, call +[`enablePendingPurchases()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.Builder#enablependingpurchases) +as part of initializing your app. -Use [`Purchase.getPurchaseState()`](https://developer.android.com/reference/com/android/billingclient/api/Purchase#getpurchasestate) to determine whether the purchase state is `PURCHASED` or `PENDING`. Note that you should grant entitlement only when the state is `PURCHASED`. You should check for `Purchase` status updates by doing the following: +Use +[`Purchase.getPurchaseState()`](https://developer.android.com/reference/com/android/billingclient/api/Purchase#getpurchasestate) +to determine whether the purchase state is `PURCHASED` or `PENDING`. Note that +you should grant entitlement only when the state is `PURCHASED`. You should +check for `Purchase` status updates by doing the following: 1. When starting your app, call [`BillingClient.queryPurchases()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#querypurchases) to retrieve the list of unconsumed products associated with the user. 2. Call `Purchase.getPurchaseState()` on each returned `Purchase` object. 3. Implement the [`onPurchasesUpdated()`](https://developer.android.com/reference/com/android/billingclient/api/PurchasesUpdatedListener#onpurchasesupdated) method to respond to changes to `Purchase` objects. -In addition, the Google Play Developer API includes a `PENDING` state for [`Purchases.products`](https://developers.google.com/android-publisher/api-ref/purchases/products). Pending transactions are not supported for subscriptions. +In addition, the Google Play Developer API includes a `PENDING` state for +[`Purchases.products`](https://developers.google.com/android-publisher/api-ref/purchases/products). +Pending transactions are not supported for subscriptions. -This release also introduces a new real-time developer notification type, `OneTimeProductNotification`. This notification type contains a single message whose value is either `ONE_TIME_PRODUCT_PURCHASED` or `ONE_TIME_PRODUCT_CANCELED`. This notification type is sent only for purchases associated with delayed forms of payment, such as cash. +This release also introduces a new real-time developer notification type, +`OneTimeProductNotification`. This notification type contains a single message +whose value is either `ONE_TIME_PRODUCT_PURCHASED` or +`ONE_TIME_PRODUCT_CANCELED`. This notification type is sent only for purchases +associated with delayed forms of payment, such as cash. -When acknowledging pending purchases, be sure to acknowledge only when the purchase state is `PURCHASED` and not `PENDING`. +When acknowledging pending purchases, be sure to acknowledge only when the +purchase state is `PURCHASED` and not `PENDING`. > [!NOTE] > **Note:** Pending transactions can be tested using license testers. In addition to two test credit cards, license testers have access to two new test instruments for delayed forms of payment which automatically complete or cancel after a couple of minutes. While testing your application, you should verify that your application does not grant entitlement or acknowledge the purchase immediately after purchasing with either of these two new instruments. When purchasing using the new test instrument that automatically completes, you should verify that your application grants entitlement and acknowledges the purchase once the purchase completes. ### API changes -Version 2.0 of the Google Play Billing library contains several API changes to support new features and clarify existing functionality. +Version 2.0 of the Google Play Billing library contains several API changes to +support new features and clarify existing functionality. #### consumeAsync -[`consumeAsync()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#consumeasync) now takes a [`ConsumeParams`](https://developer.android.com/reference/com/android/billingclient/api/ConsumeParams) object instead of a `purchaseToken`. `ConsumeParams` contains the `purchaseToken` as well as an optional developer payload. +[`consumeAsync()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#consumeasync) +now takes a +[`ConsumeParams`](https://developer.android.com/reference/com/android/billingclient/api/ConsumeParams) object +instead of a `purchaseToken`. `ConsumeParams` contains the `purchaseToken` as +well as an optional developer payload. The previous version of `consumeAsync()` has been removed in this release. #### queryPurchaseHistoryAsync -To minimize confusion, [`queryPurchaseHistoryAsync()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#queryPurchaseHistoryAsync(java.lang.String,%20com.android.billingclient.api.PurchaseHistoryResponseListener)) now returns a [`PurchaseHistoryRecord`](https://developer.android.com/reference/com/android/billingclient/api/PurchaseHistoryRecord) object instead of a `Purchase` object. The `PurchaseHistoryRecord` object is the same as a `Purchase` object, except that it reflects only the values returned by `queryPurchaseHistoryAsync()` and does not contain the `autoRenewing`, `orderId`, and `packageName` fields. Note that nothing has changed with the returned data---`queryPurchaseHistoryAsync()` returns the same data as before. +To minimize confusion, +[`queryPurchaseHistoryAsync()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#queryPurchaseHistoryAsync(java.lang.String,%20com.android.billingclient.api.PurchaseHistoryResponseListener)) +now returns a +[`PurchaseHistoryRecord`](https://developer.android.com/reference/com/android/billingclient/api/PurchaseHistoryRecord) +object instead of a `Purchase` object. The `PurchaseHistoryRecord` object is the +same as a `Purchase` object, except that it reflects only the values returned by +`queryPurchaseHistoryAsync()` and does not contain the `autoRenewing`, +`orderId`, and `packageName` fields. Note that nothing has changed with the +returned data---`queryPurchaseHistoryAsync()` returns the same data as +before. #### BillingResult return values -APIs that previously returned a `BillingResponse` integer value now return a [`BillingResult`](https://developer.android.com/reference/com/android/billingclient/api/BillingResult) object. `BillingResult` contains the `BillingResponse` integer as well as a debug string that you can use to diagnose errors. The debug string uses an en-US locale and is not meant to be shown to end users. +APIs that previously returned a `BillingResponse` integer value now return a +[`BillingResult`](https://developer.android.com/reference/com/android/billingclient/api/BillingResult) +object. `BillingResult` contains the `BillingResponse` integer as well as a +debug string that you can use to diagnose errors. The debug string uses an en-US +locale and is not meant to be shown to end users. ### Bug fixes @@ -563,7 +822,8 @@ APIs that previously returned a `BillingResponse` integer value now return a [`B ## Google Play Billing Library 1.2.2 Release (2019-03-07) -Version 1.2.2 of the Google Play Billing library is now available. This version contains the following changes. +Version 1.2.2 of the Google Play Billing library is now available. This version +contains the following changes. ### Bug fixes @@ -576,11 +836,14 @@ Version 1.2.2 of the Google Play Billing library is now available. This version ## Play Billing Library 1.2.1 Release (2019-03-04) -Version 1.2.1 of the Google Play Billing library is now available. This version contains the following changes. +Version 1.2.1 of the Google Play Billing library is now available. This version +contains the following changes. ### Major changes -- Added support for [rewarded products](https://developer.android.com/google/play/billing/billing_rewarded_products). For more information on monetization options, see [Add rewarded-product-specific features](https://developer.android.com/distribute/best-practices/earn/monetization-options). +- Added support for [rewarded + products](https://developer.android.com/google/play/billing/billing_rewarded_products). For more information on monetization options, see [Add rewarded-product-specific + features](https://developer.android.com/distribute/best-practices/earn/monetization-options). ### Other changes @@ -594,11 +857,13 @@ Version 1.2.1 of the Google Play Billing library is now available. This version ## Google Play Billing Library 1.2 Release (2018-10-18) -Version 1.2 of the Google Play Billing library is now available. This version contains the following changes. +Version 1.2 of the Google Play Billing library is now available. This version +contains the following changes. ### Summary of changes -- The Google Play Billing Library is now licensed under the [Android Software Development Kit License Agreement](https://developer.android.com/studio/terms). +- The Google Play Billing Library is now licensed under the [Android Software + Development Kit License Agreement](https://developer.android.com/studio/terms). - Added the `launchPriceChangeConfirmationFlow` API, which prompts users to review a pending change to a subscription price. - Added support for a new proration mode, `DEFERRED`, when upgrading or downgrading a user's subscription. - In the `BillingFlowParams` class, replaced `setSku()` with `setSkuDetails()`. @@ -606,9 +871,14 @@ Version 1.2 of the Google Play Billing library is now available. This version co #### Price change confirmation -You can now change the price of a subscription in Google Play Console and prompt users to review and accept the new price when they enter your app. +You can now change the price of a subscription in Google Play Console +and prompt users to review and accept the new price when they enter your app. -To use this API, create a `PriceChangeFlowParams` object by using the `skuDetails` of the subscription product, and then call `launchPriceChangeConfirmationFlow()`. Implement the `PriceChangeConfirmationListener` to handle the result when the price change confirmation flow finishes, as shown in the following code snippet: +To use this API, create a `PriceChangeFlowParams` object by using the +`skuDetails` of the subscription product, and then call +`launchPriceChangeConfirmationFlow()`. Implement the +`PriceChangeConfirmationListener` to handle the result when the price change +confirmation flow finishes, as shown in the following code snippet: ### Kotlin @@ -644,19 +914,32 @@ billingClient.launchPriceChangeConfirmationFlow(activity, }); ``` -The price change confirmation flow displays a dialog containing the new pricing information, asking users to accept the new price. This flow returns a response code of type [`BillingClient.BillingResponse`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.BillingResponse). +The price change confirmation flow displays a dialog containing the new pricing +information, asking users to accept the new price. This flow returns a response +code of type +[`BillingClient.BillingResponse`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.BillingResponse). #### New proration mode -When upgrading or downgrading a user's subscription, you can use a new proration mode, `DEFERRED`. This mode updates the user's subscription when it next renews. To learn more about how to set this proration mode, see [Set proration mode](https://developer.android.com/google/play/billing/billing_subscriptions#set-proration-mode). +When upgrading or downgrading a user's subscription, you can use a new proration +mode, `DEFERRED`. This mode updates the user's subscription when it next renews. +To learn more about how to set this proration mode, see [Set proration +mode](https://developer.android.com/google/play/billing/billing_subscriptions#set-proration-mode). #### New method for setting SKU details -In the `BillingFlowParams` class, the `setSku()` method has been deprecated. This change serves to optimize the Google Play Billing flow. +In the `BillingFlowParams` class, the `setSku()` method has been deprecated. +This change serves to optimize the Google Play Billing flow. -When constructing a new instance of `BillingFlowParams` in your in-app billing client, we recommend that you instead work with the JSON object directly using `setSkuDetails()`, as shown in the following code snippet: +When constructing a new instance of `BillingFlowParams` in your in-app billing +client, we recommend that you instead work with the JSON object directly using +`setSkuDetails()`, as shown in the following code snippet: -In the `BillingFlowParams` Builder class, the `setSku()` method has been deprecated. Instead, use the `setSkuDetails()` method, as shown in the following code snippet. The object passed into `setSkuDetails()` object comes from the [`querySkuDetailsAsync()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#querySkuDetailsAsync(com.android.billingclient.api.SkuDetailsParams,%20com.android.billingclient.api.SkuDetailsResponseListener)) method. +In the `BillingFlowParams` Builder class, the `setSku()` method has been +deprecated. Instead, use the `setSkuDetails()` method, as shown in the following +code snippet. The object passed into `setSkuDetails()` object comes from the +[`querySkuDetailsAsync()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#querySkuDetailsAsync(com.android.billingclient.api.SkuDetailsParams,%20com.android.billingclient.api.SkuDetailsResponseListener)) +method. ### Kotlin @@ -715,7 +998,8 @@ private void startPurchase(String skuId) { ## Play Billing Library 1.1 Release (2018-05-07) -Version 1.1 of the Google Play Billing library is now available. This version contains the following changes. +Version 1.1 of the Google Play Billing library is now available. This version +contains the following changes. ### Summary of changes @@ -725,11 +1009,13 @@ Version 1.1 of the Google Play Billing library is now available. This version co ### Behavior changes -Version 1.1 of the Google Play Billing library contains the following behavior changes. +Version 1.1 of the Google Play Billing library contains the following behavior +changes. #### Developers can set `replaceSkusProrationMode` in [`BillingFlowParams`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams) class -A `ProrationMode` provides further details on the type of proration when upgrading or downgrading a user's subscription. +A `ProrationMode` provides further details on the type of proration when +upgrading or downgrading a user's subscription. ### Kotlin @@ -768,11 +1054,20 @@ Google Play supports following proration modes: ### `replaceSkusProration` is no longer supported in [`BillingFlowParams`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.html) class -Developers used to be able to set a boolean flag to charge a prorated amount for a subscription upgrade request. Given that we are supporting `ProrationMode`, which contains more detailed proration instruction, this boolean flag is no longer supported. +Developers used to be able to set a boolean flag to charge a prorated amount for +a subscription upgrade request. Given that we are supporting `ProrationMode`, +which contains more detailed proration instruction, this boolean flag is no +longer supported. ### [`launchBillingFlow()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.html#launchBillingFlow(android.app.Activity,%20com.android.billingclient.api.BillingFlowParams)) now triggers a callback for failed responses -The Billing Library will always trigger the [`PurhcasesUpdatedListener`](https://developer.android.com/reference/com/android/billingclient/api/PurchasesUpdatedListener) callback and return a [`BillingResponse`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.BillingResponse) asynchronously. The synchronous return value of [`BillingResponse`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.BillingResponse) is kept as well. +The Billing Library will always trigger the +[`PurhcasesUpdatedListener`](https://developer.android.com/reference/com/android/billingclient/api/PurchasesUpdatedListener) +callback and return a +[`BillingResponse`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.BillingResponse) +asynchronously. The synchronous return value of +[`BillingResponse`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.BillingResponse) +is kept as well. ### Bug fixes @@ -782,7 +1077,8 @@ The Billing Library will always trigger the [`PurhcasesUpdatedListener`](https:/ ## Google Play Billing Library 1.0 Release (2017-09-19, [Announcement](https://android-developers.googleblog.com/2017/09/google-play-billing-library-10-released.html)) -Version 1.0 of the Google Play Billing library is now available. This version contains the following changes. +Version 1.0 of the Google Play Billing library is now available. This version +contains the following changes. ### Important changes @@ -793,11 +1089,13 @@ Version 1.0 of the Google Play Billing library is now available. This version co ### Behavior changes -Version 1.0 of the Google Play Billing library contains the following behavior changes. +Version 1.0 of the Google Play Billing library contains the following behavior +changes. #### BillingClient.Builder class -[`BillingClient.Builder`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.Builder.html) is now initialized via the newBuilder pattern: +[`BillingClient.Builder`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.Builder.html) +is now initialized via the newBuilder pattern: ### Kotlin @@ -813,7 +1111,10 @@ billingClient = BillingClient.newBuilder(context).setListener(this).build(); #### launchBillingFlow method is now called using a BillingFlowParams class -To initiate the billing flow for a purchase or subscription, the [`launchBillingFlow()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.html#launchBillingFlow(android.app.Activity,%0Acom.android.billingclient.api.BillingFlowParams)) method receives a [`BillingFlowParams`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.html) instance initialized with parameters specific to the request: +To initiate the billing flow for a purchase or subscription, the +[`launchBillingFlow()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.html#launchBillingFlow(android.app.Activity,%0Acom.android.billingclient.api.BillingFlowParams)) method receives a +[`BillingFlowParams`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.html) +instance initialized with parameters specific to the request: ### Kotlin @@ -841,7 +1142,10 @@ int responseCode = billingClient.launchBillingFlow(builder.build()); #### New way to query available products -Arguments for [`queryPurchaseHistoryAsync()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.html#queryPurchaseHistoryAsync(java.lang.String,%0Acom.android.billingclient.api.PurchaseHistoryResponseListener)) and [`querySkuDetailsAsync()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.html#querySkuDetailsAsync(com.android.billingclient.api.SkuDetailsParams,%0Acom.android.billingclient.api.SkuDetailsResponseListener)) methods were wrapped into a Builder pattern: +Arguments for +[`queryPurchaseHistoryAsync()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.html#queryPurchaseHistoryAsync(java.lang.String,%0Acom.android.billingclient.api.PurchaseHistoryResponseListener)) and +[`querySkuDetailsAsync()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.html#querySkuDetailsAsync(com.android.billingclient.api.SkuDetailsParams,%0Acom.android.billingclient.api.SkuDetailsResponseListener)) methods were wrapped +into a Builder pattern: ### Kotlin @@ -863,7 +1167,10 @@ params.setSkusList(skuList) billingClient.querySkuDetailsAsync(params.build(), new SkuDetailsResponseListener() {...}) ``` -The result is now returned via result code and a list of [`SkuDetails`](https://developer.android.com/reference/com/android/billingclient/api/SkuDetails.html) objects instead of previous wrapper class for your convenience and to be consistent across our API: +The result is now returned via result code and a list of +[`SkuDetails`](https://developer.android.com/reference/com/android/billingclient/api/SkuDetails.html) +objects instead of previous wrapper class for your convenience and to be +consistent across our API: ### Kotlin @@ -879,7 +1186,10 @@ public void onSkuDetailsResponse(@BillingResponse int responseCode, List ### Bug fixes - [No response code in PURCHASES_UPDATED Bundle](https://issuetracker.google.com/issues/64075043) -- [Fix ProxyBillingActivity and PurchasesUpdatedListener issues during device rotation](https://issuetracker.google.com/issues/63266562) +- [Fix ProxyBillingActivity and PurchasesUpdatedListener issues during device + rotation](https://issuetracker.google.com/issues/63266562) ## Developer Preview 1 Release (2017-06-12, [Announcement](https://android-developers.googleblog.com/2017/06/money-made-easily-with-new-google-play.html)) -Developer preview launched, aimed to simplify the development process when it comes to billing, allowing developers to focus their efforts on implementing logic specific to the Android app, such as application architecture and navigation structure. +Developer preview launched, aimed to simplify the development process when it +comes to billing, allowing developers to focus their efforts on implementing +logic specific to the Android app, such as application architecture and +navigation structure. -The library includes several convenient classes and features for you to use when integrating your Android apps with the Google Play Billing API. The library also provides an abstraction layer on top of the Android Interface Definition Language (AIDL) service, making it easier for developers to define the interface between the app and the Google Play Billing API. \ No newline at end of file +The library includes several convenient classes and features for you to use when +integrating your Android apps with the Google Play Billing API. The library also +provides an abstraction layer on top of the Android Interface Definition +Language (AIDL) service, making it easier for developers to define the interface +between the app and the Google Play Billing API. \ No newline at end of file diff --git a/play/play-billing-library-version-upgrade/references/migration-logic.md b/play/play-billing-library-version-upgrade/references/migration-logic.md index b0a095a..227a537 100644 --- a/play/play-billing-library-version-upgrade/references/migration-logic.md +++ b/play/play-billing-library-version-upgrade/references/migration-logic.md @@ -2,7 +2,8 @@ ### 1. Connection Management (The v8+ Reconnection Shift) -**Intent**: Move from developer-managed state (manual retries) to library-managed state. +**Intent**: Move from developer-managed state (manual retries) to +library-managed state. - **Remove** : Manual `startConnection()` calls or retry timers inside `onServiceDisconnected()`. - **Add** : `.enableAutoServiceReconnection()` to `BillingClient.Builder`. @@ -10,7 +11,8 @@ ### 2. Product Querying \& Models (The v5-v8 Architectural Shift) -**Intent**: Support the "One Product, Multiple Offers" model introduced in v5 and refined in v8. +**Intent**: Support the "One Product, Multiple Offers" model introduced in v5 +and refined in v8. - **Data Model Swap** : - **Legacy** : `SkuDetails` (1:1 mapping of ID to price). @@ -18,7 +20,13 @@ - **Result Handling (v8+ Logic)** : - **Change** : `queryProductDetailsAsync` no longer returns a list in the listener. - **New Intent** : You must receive a `QueryProductDetailsResult` object. - - **Refactor** : `kotlin // PBL 8+ Pattern billingClient.queryProductDetailsAsync(params) { result: QueryProductDetailsResult -> val responseCode = result.billingResult.responseCode val productDetailsList = result.productDetailsList // Retrieve list from result object // Process list... }` + - **Refactor** : `kotlin + // PBL 8+ Pattern + billingClient.queryProductDetailsAsync(params) { result: QueryProductDetailsResult -> + val responseCode = result.billingResult.responseCode + val productDetailsList = result.productDetailsList // Retrieve list from result object + // Process list... + }` ### 3. Subscription Modernization (v6 \& v7) @@ -36,7 +44,8 @@ ### 4. Purchase Handling \& History (v6+) -**Intent**: Move away from local-only purchase caches to real-time status checks. +**Intent**: Move away from local-only purchase caches to real-time status +checks. - **Active Purchases** : - **Deprecated** : `queryPurchases()` (synchronous). @@ -60,7 +69,8 @@ ### 7. User-Facing Features (Post-Upgrade Recommendations) -Once the upgrade is complete, the following features are enabled by these versions: +Once the upgrade is complete, the following features are enabled by these +versions: - **v7** : **Installments** (Monthly payments for annual plans). - **v8** : **Prepaid Plans** (Users can top-up time without auto-renewing). diff --git a/play/play-billing-library-version-upgrade/references/version-checklist.md b/play/play-billing-library-version-upgrade/references/version-checklist.md index 47005eb..98f5831 100644 --- a/play/play-billing-library-version-upgrade/references/version-checklist.md +++ b/play/play-billing-library-version-upgrade/references/version-checklist.md @@ -1,6 +1,7 @@ ## Play Billing Library: Smart Version-Specific Checklist -Use this checklist to verify that every technical requirement between your \[Current Version\] and \[Target Version\] has been met. +Use this checklist to verify that every technical requirement between your +\[Current Version\] and \[Target Version\] has been met. ## PBL v1.x through v3.x diff --git a/security/android-intent-security/SKILL.md b/security/android-intent-security/SKILL.md index b1f5f42..e1e8d55 100644 --- a/security/android-intent-security/SKILL.md +++ b/security/android-intent-security/SKILL.md @@ -7,7 +7,7 @@ description: Best practices for Android Intent security. Use this skill when aud license: Complete terms in LICENSE.txt metadata: author: Google LLC - last-updated: '2026-08-06' + last-updated: '2026-08-14' keywords: - recipe - Android @@ -24,7 +24,9 @@ metadata: - Best Practices --- -This skill provides guidelines and patterns to secure Android components (Activities, Services, Broadcast Receivers, Content Providers) and handle Intents safely, preventing privilege escalation and unauthorized access. +This skill provides guidelines and patterns to secure Android components +(Activities, Services, Broadcast Receivers, Content Providers) and handle +Intents safely, preventing privilege escalation and unauthorized access. ## Glossary @@ -82,19 +84,45 @@ Evaluate the security implications of PendingIntent mutability flags: ### 3. Intent handling and redirection logic -IF (the component receives a nested Intent as an extra) { IF (AndroidX Core 1.9.0+ and higher is available) { MUST construct an `IntentSanitizer` to explicitly allowlist components, actions, data, and extras. MUST call `sanitizeByThrowing()` or `sanitizeByFiltering()` before launching. } ELSE { MUST verify that the nested Intent's target package matches the current application package. MUST verify that the target component of the nested Intent is publicly exported. } NEVER launch the nested Intent directly without validation. } ELSE IF (the component handles broadcasts) { MUST rely on the system's Protected Broadcast mechanism for system events (which guarantees the sender is the system framework). MUST protect custom receivers with signature-level permissions or use `RECEIVER_NOT_EXPORTED` for dynamic receivers to restrict the sender. } +IF (the component receives a nested Intent as an extra) { +IF (AndroidX Core 1.9.0+ and higher is available) { +MUST construct an `IntentSanitizer` to explicitly allowlist components, actions, data, and extras. +MUST call `sanitizeByThrowing()` or `sanitizeByFiltering()` before launching. +} ELSE { +MUST verify that the nested Intent's target package matches the current application package. +MUST verify that the target component of the nested Intent is publicly exported. +} +NEVER launch the nested Intent directly without validation. +} ELSE IF (the component handles broadcasts) { +MUST rely on the system's Protected Broadcast mechanism for system events (which guarantees the sender is the system framework). +MUST protect custom receivers with signature-level permissions or use `RECEIVER_NOT_EXPORTED` for dynamic receivers to restrict the sender. +} ### 4. PendingIntent security logic -IF (a PendingIntent is created for delivery to another application) { MUST use `PendingIntent.FLAG_IMMUTABLE` by default. IF (the PendingIntent must be mutable) { MUST set the explicit target component or package name on the base `Intent`. NEVER create an implicit, mutable `PendingIntent`. } } +IF (a PendingIntent is created for delivery to another application) { +MUST use `PendingIntent.FLAG_IMMUTABLE` by default. +IF (the PendingIntent must be mutable) { +MUST set the explicit target component or package name on the base `Intent`. +NEVER create an implicit, mutable `PendingIntent`. +} +} ### 5. ContentProvider security logic -IF (the ContentProvider is only for internal app use) { MUST set `android:exported="false"`. } ELSE { MUST protect it with `android:readPermission` and `android:writePermission`. MUST set `android:grantUriPermissions="false"` unless temporary URL access is strictly required. } +IF (the ContentProvider is only for internal app use) { +MUST set `android:exported="false"`. +} ELSE { +MUST protect it with `android:readPermission` and `android:writePermission`. +MUST set `android:grantUriPermissions="false"` unless temporary URL access is strictly required. +} ### 6. Service caller verification logic -IF (an exported service communicates with trusted sister/partner apps) { MUST retrieve the calling UID using `Binder.getCallingUid()` and resolve it to package names using `PackageManager.getPackagesForUid()`. MUST verify that the calling package signature fingerprint matches your trusted certificate hash. } +IF (an exported service communicates with trusted sister/partner apps) { +MUST retrieve the calling UID using `Binder.getCallingUid()` and resolve it to package names using `PackageManager.getPackagesForUid()`. +MUST verify that the calling package signature fingerprint matches your trusted certificate hash. +} *** ** * ** *** @@ -102,14 +130,14 @@ IF (an exported service communicates with trusted sister/partner apps) { MUST re ### 1. Safe intent redirection (manual verification) -Validate the target of a nested intent before launching it when modern sanitization libraries are unavailable. +Validate the target of a nested intent before launching it when modern +sanitization libraries are unavailable. - **Expected Inputs:** - An incoming `Intent` containing a nested `Intent` extra named `EXTRA_NESTED_INTENT`. - **Expected Outputs:** - Launches the target component if safe; throws `SecurityException` if validation fails. -
```kotlin fun safeIntentRedirectionManual() { @@ -151,21 +179,20 @@ fun safeIntentRedirectionManual() { } } } - ```
### 2. Safe intent redirection using IntentSanitizer -Filter or reject dynamic intents using AndroidX `IntentSanitizer` (AndroidX Core 1.9.0+). +Filter or reject dynamic intents using AndroidX `IntentSanitizer` (AndroidX Core +1.9.0+). - **Expected Inputs:** - An untrusted incoming `Intent`. - **Expected Outputs:** - `Intent`: A sanitized copy containing only allowlisted components, categories, and actions. Throws `SecurityException` on violations if using `sanitizeByThrowing()`. -
```kotlin fun safeIntentRedirectionSanitizer() { @@ -194,30 +221,27 @@ fun safeIntentRedirectionSanitizer() { // startActivity(filteredIntent) } } - ```
### 3. Custom signature permission protection -Declare a custom signature-level permission in the manifest to secure family app communication. +Declare a custom signature-level permission in the manifest to secure family app +communication. - **Expected Inputs:** Manifest configuration. - **Expected Outputs:** An activity that can only be launched by apps signed with the same developer certificate. -
```xml - ```
-
```xml - ```
### 4. Safe onNewIntent lifecycle verification (warm boot protection) -Ensure that activities reusing dynamic intents (for example, in background launch paths) apply the same strict security filters inside `onNewIntent`. +Ensure that activities reusing dynamic intents (for example, in background +launch paths) apply the same strict security filters inside `onNewIntent`. - **Expected Inputs:** - `newIntent` (`Intent`): The newly delivered intent. - **Expected Outputs:** - Executes processing logic only if the new intent passes security validation. -
```kotlin override fun onNewIntent(newIntent: Intent) { @@ -263,7 +286,6 @@ override fun onNewIntent(newIntent: Intent) { private fun validateIntent(intent: Intent): Boolean { return intent.hasExtra("VALID_PAYLOAD_MARKER") } - ```
@@ -277,7 +299,6 @@ Enforce immutability unless mutability is explicitly required. - **Expected Inputs (Mutable):** An intent with an explicit component set. - **Expected Outputs (Mutable):** A mutable `PendingIntent` locked to a specific receiver component to prevent hijacking. -
```kotlin fun createPendingIntents(context: Context) { @@ -302,14 +323,14 @@ fun createPendingIntents(context: Context) { PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT ) } - ```
### 6. Secure ContentProvider configuration and queries -Expose a ContentProvider securely and parameterize queries to prevent SQL injection. +Expose a ContentProvider securely and parameterize queries to prevent SQL +injection. - **Expected Inputs:** - `uri` (`Uri`): The query URI. @@ -319,7 +340,6 @@ Expose a ContentProvider securely and parameterize queries to prevent SQL inject - **Expected Outputs:** - `Cursor`: Filtered query results, strictly bound to projection maps. -
```xml - ```
-
```kotlin override fun query( @@ -360,7 +378,6 @@ override fun query( val db = dbHelper.readableDatabase return queryBuilder.query(db, projection, selection, selectionArgs, null, null, sortOrder) } - ```
@@ -374,7 +391,6 @@ Verify the calling application's signature before binding to a service. - **Expected Outputs:** - `IBinder`: Local binder instance if caller signature matches trusted partner; throws `SecurityException` otherwise. -
```kotlin class SecureBoundService : Service() { @@ -423,7 +439,6 @@ class SecureBoundService : Service() { } } } - ```
@@ -432,9 +447,9 @@ class SecureBoundService : Service() { ## Error handling -Handle component binding, database queries, and intent redirection failures securely to avoid exposing internal structures. +Handle component binding, database queries, and intent redirection failures +securely to avoid exposing internal structures. -
```kotlin fun safeErrorHandling(callingPackage: String?) { @@ -454,7 +469,6 @@ fun safeErrorHandling(callingPackage: String?) { Log.w("INTEGRITY_WARNING", "Missing intent parameter", e) } } - ```
@@ -471,7 +485,10 @@ fun safeErrorHandling(callingPackage: String?) { ## Reporting guidelines -When this skill is executed to apply security hardening updates to a codebase, the agent **MUST** generate a structured "Best Practices and Security Alignment Update" report for the developer. The report **must** be written to the session artifact folder (or printed in the final response) and include: +When this skill is executed to apply security hardening updates to a codebase, +the agent **MUST** generate a structured "Best Practices and Security Alignment +Update" report for the developer. The report **must** be written to the session +artifact folder (or printed in the final response) and include: 1. **Security alignment area:** The category of improvement applied (for example, Safe Intent Redirection, Secure PendingIntent Configuration, ContentProvider Data Guarding). 2. **Impact and priority:** The potential safety risk addressed by the update (for example, Component Hijacking Prevention, Private Data Isolation). diff --git a/system/edge-to-edge/SKILL.md b/system/edge-to-edge/SKILL.md index ef19d2c..c77ef1b 100644 --- a/system/edge-to-edge/SKILL.md +++ b/system/edge-to-edge/SKILL.md @@ -7,7 +7,7 @@ description: Use this skill to migrate your Jetpack Compose app to add adaptive license: Complete terms in LICENSE.txt metadata: author: Google LLC - last-updated: '2026-08-06' + last-updated: '2026-08-14' keywords: - android - compose @@ -35,11 +35,11 @@ metadata: ## Step 3: apply insets -- The app **MUST** apply system insets, or align content to rulers, so critical UI remains tappable. Choose only one method to avoid double padding: +- The app **MUST** apply system insets, or align content to rulers, so critical + UI remains tappable. Choose only one method to avoid double padding: 1. **PREFERRED:** When available, use `Scaffold`s and pass `PaddingValues` to the content lambda. -
```kotlin Scaffold { innerPadding -> @@ -51,7 +51,6 @@ metadata: contentPadding = innerPadding ) { /* Content */ } } - ```
@@ -77,7 +76,6 @@ metadata: 3. `TopAppBar(windowInsets = WindowInsets.systemBars.add(WindowInsets.captionBar))` 2. For components outside a Scaffold, use padding modifiers, such as `Modifier.safeDrawingPadding()` or `Modifier.windowInsetsPadding(WindowInsets.safeDrawing)`. -
```kotlin Box( @@ -92,14 +90,16 @@ metadata: Text("Login") } } - ```
3. For deeply nested components with excessive padding, use `WindowInsetsRulers` (e.g. `Modifier.fitInside(WindowInsetsRulers.SafeDrawing.current)`). See the *IME* section for a code sample. - 4. When you need an element (e.g. a custom header or decorative scrim) to equal the dimensions of a system bar, use inset size modifiers (e.g. `Modifier.windowInsetsTopHeight(WindowInsets.systemBars)`). See the *Lists* section for a code sample. + 4. When you need an element (e.g. a custom header or decorative scrim) to + equal the dimensions of a system bar, use inset size modifiers (e.g. + `Modifier.windowInsetsTopHeight(WindowInsets.systemBars)`). + See the *Lists* section for a code sample. ## Adaptive Scaffolds @@ -109,13 +109,16 @@ metadata: - For each Activity with a soft keyboard, check that `android:windowSoftInputMode="adjustResize"` is set in the AndroidManifest.xml. DO NOT use `SOFT_INPUT_ADJUST_RESIZE` because it is deprecated. Then, maintain focus on the input field. Choose one: - 1. **PREFERRED:** Add `Modifier.fitInside(WindowInsetsRulers.Ime.current)` to the content container. This is preferred over `imePadding()` because it reduces jank and extra padding caused by forgetting to consume insets upstream in the hierarchy. - - 2. Add `imePadding` to the content container. The padding modifier **MUST** be placed before `Modifier.verticalScroll()`. Do NOT use `Modifier.imePadding()` if the parent already accounts for the IME with `contentWindowInsets` (e.g. `contentWindowInsets = WindowInsets.safeDrawing`). Doing so will cause double padding. + - 2. Add `imePadding` to the content container. The padding modifier **MUST** be placed before `Modifier.verticalScroll()`. Do NOT use `Modifier.imePadding()` if the parent already accounts for the IME with `contentWindowInsets` (e.g. `contentWindowInsets = + WindowInsets.safeDrawing`). Doing so will cause double padding. ### IMEs with Scaffolds code patterns #### RIGHT -RIGHT because `contentWindowInsets` contains IME insets, which are passed to the content lambda as `innerPadding`. +RIGHT because `contentWindowInsets` contains IME insets, which are passed to the +content lambda as `innerPadding`. + ```kotlin // RIGHT @@ -127,14 +130,15 @@ Scaffold(contentWindowInsets = WindowInsets.safeDrawing) { innerPadding -> .verticalScroll(rememberScrollState()) ) { /* Content */ } } - ```
*** ** * ** *** -RIGHT because `fitInside` fits the content to the IME insets regardless of `contentWindowInsets`. +RIGHT because `fitInside` fits the content to the IME insets regardless of +`contentWindowInsets`. + ```kotlin // RIGHT @@ -147,14 +151,15 @@ Scaffold() { innerPadding -> .verticalScroll(rememberScrollState()) ) { /* Content */ } } - ```
*** ** * ** *** -RIGHT because the default `contentWindowInsets` does not contain IME insets, and `imePadding()` applies IME insets: +RIGHT because the default `contentWindowInsets` does not contain IME insets, and +`imePadding()` applies IME insets: + ```kotlin // RIGHT @@ -167,14 +172,16 @@ Scaffold() { innerPadding -> .verticalScroll(rememberScrollState()) ) { /* Content */ } } - ```
#### WRONG -WRONG because there will be excess padding when the IME opens. IME insets are applied twice, once with innerPadding, which contains IME insets from the passed `contentWindowInsets` values, and once with `imePadding`: +WRONG because there will be excess padding when the IME opens. IME insets are +applied twice, once with innerPadding, which contains IME insets from the passed +`contentWindowInsets` values, and once with `imePadding`: + ```kotlin // WRONG @@ -186,14 +193,15 @@ Scaffold( contentWindowInsets = WindowInsets.safeDrawing ) { innerPadding -> .verticalScroll(rememberScrollState()) ) { /* Content */ } } - ```
*** ** * ** *** -WRONG because the IME will cover up the content. Scaffold's default `contentWindowInsets` does NOT contain IME insets. +WRONG because the IME will cover up the content. Scaffold's default +`contentWindowInsets` does NOT contain IME insets. + ```kotlin // WRONG @@ -204,7 +212,6 @@ Scaffold() { innerPadding -> .verticalScroll(rememberScrollState()) ) { /* Content */ } } - ```
@@ -215,6 +222,7 @@ Scaffold() { innerPadding -> The following code samples WILL NOT cause excessive padding. + ```kotlin // RIGHT Box( @@ -225,14 +233,12 @@ Box( modifier = Modifier.imePadding() ) { /* Content */ } } - ```
*** ** * ** *** -
```kotlin // RIGHT @@ -244,14 +250,12 @@ Box( modifier = Modifier.imePadding() ) { /* Content */ } } - ```
*** ** * ** *** -
```kotlin // RIGHT @@ -265,14 +269,15 @@ Box( .fitInside(WindowInsetsRulers.Ime.current) ) { /* Content */ } } - ```
#### WRONG -The following code sample WILL cause excessive padding because IME insets are applied twice: +The following code sample WILL cause excessive padding because IME insets are +applied twice: + ```kotlin // WRONG @@ -284,16 +289,19 @@ Box( modifier = Modifier.imePadding() ) { /* Content */ } } - ```
## Navigation Bar Contrast \& System Bar Icons -- If the Activity uses `enableEdgeToEdge` from `WindowCompat`, you **MUST** set `isAppearanceLightNavigationBars` and `isAppearanceLightStatusBars` to the inverse of the device theme for apps that support light and dark theme so the system bar icons are legible. It's recommended to do this in your theme file. DO NOT do this if the Activities use `enableEdgeToEdge` from `ComponentActivity` because it handles the icon colors automatically. +- If the Activity uses `enableEdgeToEdge` from `WindowCompat`, you **MUST** set + `isAppearanceLightNavigationBars` and `isAppearanceLightStatusBars` to the + inverse of the device theme for apps that support light and dark theme so the + system bar icons are legible. It's recommended to do this in your theme file. + DO NOT do this if the Activities use `enableEdgeToEdge` from `ComponentActivity` + because it handles the icon colors automatically. -
```kotlin // Only use if calling `enableEdgeToEdge` from `WindowCompat`. @@ -317,19 +325,22 @@ Box( MaterialTheme(content = content) } - ```
-- If any screen uses a `Scaffold` or a `NavigationSuiteScaffold` with a bottom bar (e.g., `BottomAppBar`, `NavigationBar`), set `window.isNavigationBarContrastEnforced = false` in the corresponding Activity for SDK 29+. This prevents the system from adding a translucent background to the navigation bar, verifying your bottom bar colors extend to the bottom of the screen. +- If any screen uses a `Scaffold` or a `NavigationSuiteScaffold` with a bottom + bar (e.g., `BottomAppBar`, `NavigationBar`), set + `window.isNavigationBarContrastEnforced = false` in the corresponding Activity + for SDK 29+. This prevents the system from adding a translucent background to + the navigation bar, verifying your bottom bar colors extend to the bottom of the + screen. ## Lists - Apply inset padding (like `Scaffold`'s `innerPadding`) to the `contentPadding` parameter of scrollable components (e.g. `LazyColumn`, `LazyRow`). DO NOT apply it as a `Modifier.padding()` to the list's parent container, as this clips the content and prevents it from scrolling behind the system bars. - Create a translucent composable covering the system bar so that the icons are still legible. -
```kotlin class SystemBarProtectionSnippets : ComponentActivity() { @@ -376,18 +387,20 @@ private fun StatusBarProtection( ) ) } - ```
## Dialogs -If both the following conditions are true, then the Dialog is full screen and must be made edge-to-edge: 1. The `DialogProperties` contains `usePlatformDefaultWidth = false`. 2. The Dialog calls `Modifier.fillMaxSize()`. +If both the following conditions are true, then the Dialog is full screen and +must be made edge-to-edge: +1. The `DialogProperties` contains `usePlatformDefaultWidth = false`. +2. The Dialog calls `Modifier.fillMaxSize()`. -To make a full screen Dialog edge-to-edge, set `decorFitsSystemWindows = false` in the `DialogProperties`. +To make a full screen Dialog edge-to-edge, set `decorFitsSystemWindows = false` +in the `DialogProperties`. -
```kotlin Dialog( @@ -399,7 +412,6 @@ Dialog( decorFitsSystemWindows = false ) ) { /* Content */ } - ```
diff --git a/testing/testing-setup/SKILL.md b/testing/testing-setup/SKILL.md index 316e9d4..99a4368 100644 --- a/testing/testing-setup/SKILL.md +++ b/testing/testing-setup/SKILL.md @@ -6,7 +6,7 @@ description: Analyze and create a testing strategy for native Android apps - ins license: Complete terms in LICENSE.txt metadata: author: Google LLC - last-updated: '2026-08-06' + last-updated: '2026-08-14' keywords: - android - testing @@ -17,7 +17,8 @@ metadata: ## Step 1: analyze the current testing setup -To understand the testing setup of an existing project, look for these dependencies in the libs.versions.toml file, or build files: +To understand the testing setup of an existing project, look for these +dependencies in the libs.versions.toml file, or build files: 1. Dependency Injection framework used. Examples: Hilt, Koin, Anvil, vanilla Dagger... 2. Unit (local) testing framework this project uses, Example JUnit4, JUnit5... @@ -39,26 +40,35 @@ To understand the testing setup of an existing project, look for these dependenc ## Step 2: Set up Dependency Injection frameworks for testing -If there is no Dependency Injection framework, install one: if it's a multiplatform application, ask the user whether they want to install Koin, or kotlin-inject. If it's not multiplatform, install Hilt. +If there is no Dependency Injection framework, install one: if it's a +multiplatform application, ask the user whether they want to install Koin, or +kotlin-inject. If it's not multiplatform, install Hilt. -Install the testing dependencies (for example `com.google.dagger:hilt-compiler` that should be applied with a `kspAndroidTest` configuration). +Install the testing dependencies (for example `com.google.dagger:hilt-compiler` +that should be applied with a `kspAndroidTest` configuration). > [!IMPORTANT] > **Important:** Always consult the documentation of the applicable framework to learn about testing (for example: [Hilt testing guide](references/android/training/dependency-injection/hilt-testing.md), [Koin Instrumented tests](https://insert-koin.io/docs/reference/koin-android/instrumented-testing)). -For instrumented tests, create and configure (by adding `testInstrumentationRunner` to the build gradle files) a new test runner and apply the testing rules required by the framework (for example in Hilt, annotate your test classes with `@HiltAndroidTest` and apply the `HiltAndroidRule`). Other frameworks use other mechanisms, consult their documentation. +For instrumented tests, create and configure (by adding +`testInstrumentationRunner` to the build gradle files) a new test runner and +apply the testing rules required by the framework (for example in Hilt, annotate +your test classes with `@HiltAndroidTest` and apply the `HiltAndroidRule`). +Other frameworks use other mechanisms, consult their documentation. ## Step 3: Install frameworks Unless otherwise specified, respect the current stack of testing frameworks. -If there are no testing frameworks, and the user didn't specify any preference, install the following: +If there are no testing frameworks, and the user didn't specify any preference, +install the following: - JUnit4 for local and instrumented tests - Jacoco for test coverage - For UI tests: if the project has views, Espresso. If it's fully Compose, use the Compose Testing APIs. - Robolectric to run UI Tests -- Compose Preview Screenshot Testing tool for screenshot tests - check [setup documentation](references/android/studio/preview/compose-screenshot-testing.md) and follow it strictly. +- Compose Preview Screenshot Testing tool for screenshot tests - check [setup + documentation](references/android/studio/preview/compose-screenshot-testing.md) and follow it strictly. - Dropshots for device screenshot tests - If a mocking framework is necessary, install Mockk (`io.mockk:mockk`). Do not install it unless it is clearly necessary. @@ -70,30 +80,43 @@ If end-to-end testing is requested, install UI Automator. ### **Refactor for unit tests** -In the next sections you'll be asked to create tests. If you have dependencies on Android framework classes, or entities that are not part of the codebase: +In the next sections you'll be asked to create tests. If you have dependencies +on Android framework classes, or entities that are not part of the codebase: -- First, use a fake. If it doesn't exist, create an interface for the class and a "Default" implementation with the existing code. Add the Fake version to the test sourceset (test or androidTest). +- First, use a fake. If it doesn't exist, create an interface for the class + and a "Default" implementation with the existing code. Add the Fake version + to the test sourceset (test or androidTest). -- If not possible to use a fake (example: no access to the class or interface), mock the dependencies. +- If not possible to use a fake (example: no access to the class or + interface), mock the dependencies. ### **Refactor for UI tests** -If you need to fake components to make testing easier and faster and more reliable, replace slow and problematic dependencies with fakes. Use runtime fakes using the Dependency Injection framework installed to: +If you need to fake components to make testing easier and faster and more +reliable, replace slow and problematic dependencies with fakes. Use runtime +fakes using the Dependency Injection framework installed to: - **Simulate** different scenarios with the user (wrong credentials, reset password flow...), with a server (no connection, server down, bad JSON from server...) or with a platform component (insufficient permissions, no disk space, no front camera available) - **Improve** speed and reliability (replacing a database with an in-memory database, replacing a repository with an in-memory fake to avoid hitting the network) ## Step 5: Unit testing -Create a task to add or review unit tests in every file that contains business logic (ViewModels, Repositories, database-related classes such as DAOs, etc.). Don't create unit tests for Activities, Compose layouts, or dependency injection configuration files. +Create a task to add or review unit tests in every file that contains business +logic (ViewModels, Repositories, database-related classes such as DAOs, etc.). +Don't create unit tests for Activities, Compose layouts, or dependency injection +configuration files. ## Step 6: UI testing -Espresso or Compose UI tests live in the `test` sourceset because they will be run with Robolectric. If instrumented (emulator or device) tests are requested, put them in the `androidTest` sourceset. +Espresso or Compose UI tests live in the `test` sourceset because they will be +run with Robolectric. If instrumented (emulator or device) tests are requested, +put them in the `androidTest` sourceset. ## Step 7: Test databases -If the database is using SQLite (using Room, SQLDelight, etc.), create instrumented tests using an in-memory database to make sure that they work with the SQLite engine on device. +If the database is using SQLite (using Room, SQLDelight, etc.), create +instrumented tests using an in-memory database to make sure that they work with +the SQLite engine on device. ## Step 8: Screenshot tests @@ -105,11 +128,14 @@ Irrespective of the framework used, screenshot tests focus on 2 types of tests: - Font scale set to 1.5. - Component-level screenshot tests, where each component is tested in different themes and font scales. -Behavior isn't tested with screenshots, but do test different common scenarios if their UIs change a lot depending on the state. For example, test loading screens by injecting a loading state to the UI or simulating it with a fake. +Behavior isn't tested with screenshots, but do test different common scenarios +if their UIs change a lot depending on the state. For example, test loading +screens by injecting a loading state to the UI or simulating it with a fake. ## Step 9: UI Behavior tests -Test the UI logic using behavior tests, which ensures that the UIs react as expected when different states are passed, and when user actions are performed. +Test the UI logic using behavior tests, which ensures that the UIs react as +expected when different states are passed, and when user actions are performed. ### **Compose UI behavior tests** @@ -131,17 +157,25 @@ Create a test suite to verify navigation logic. Include: ## Step 11: Simulate different window sizes and settings -For Compose layouts, use `DeviceConfigurationOverride` described in "[UI testing common patterns](references/android/develop/ui/compose/testing/common-patterns.md)" to simulate different window sizes, font scales +For Compose layouts, use `DeviceConfigurationOverride` described in "[UI testing +common patterns](references/android/develop/ui/compose/testing/common-patterns.md)" to simulate different window sizes, font scales ## Step 12: End-to-end tests -Create a low number (about 5% of all tests) of end-to-end tests that cover big user journeys. Use Compose Test APIs or Espresso for that. If you have to access platform features (notifications, system UI...), use UI Automator. +Create a low number (about 5% of all tests) of end-to-end tests that cover big +user journeys. Use Compose Test APIs or Espresso for that. If you have to access +platform features (notifications, system UI...), use UI Automator. -If you need to take screenshots of the app running in a device, use [Dropshots](https://raw.githubusercontent.com/dropbox/dropshots/refs/heads/main/README.md). You need a device for screenshot tests when verifying interaction with the system UI (examples: edge-to-edge rendering, notifications, picture-in-picture) +If you need to take screenshots of the app running in a device, use +[Dropshots](https://raw.githubusercontent.com/dropbox/dropshots/refs/heads/main/README.md). You need a device for screenshot tests when verifying +interaction with the system UI (examples: edge-to-edge rendering, notifications, +picture-in-picture) ### Step 13: Instrumented Screenshot tests -Install the `com.dropbox.dropshots` plugin in the module and a `Dropshots()` JUnit Rule. Create a new instrumented screenshot test for one of the app's features. +Install the `com.dropbox.dropshots` plugin in the module and a `Dropshots()` +JUnit Rule. Create a new instrumented screenshot test for one of the app's +features. ### Step 14: Install jacoco @@ -151,8 +185,14 @@ Install jacoco for local testing code coverage. ## Final touches -- Ask whether to document the findings of the analysis and the changes applied to the testing strategy. If the user agrees: +- Ask whether to document the findings of the analysis and the changes applied + to the testing strategy. If the user agrees: - - If there is an AGENTS.md file present in the project, update it with any changes you've made to the testing strategy. + - If there is an AGENTS.md file present in the project, update it with any + changes you've made to the testing strategy. - - If there is no AGENTS.md file, create a new file (docs/testing.md) with a description of the testing strategy, including the commands needed to run every type of test, where the screenshot reference files live, etc. Also create a new AGENTS.md file in the root and create a link to docs/testing.md. + - If there is no AGENTS.md file, create a new file (docs/testing.md) with + a description of the testing strategy, including the commands needed to + run every type of test, where the screenshot reference files live, etc. + Also create a new AGENTS.md file in the root and create a link to + docs/testing.md. diff --git a/testing/testing-setup/references/android/develop/ui/compose/testing/common-patterns.md b/testing/testing-setup/references/android/develop/ui/compose/testing/common-patterns.md index f0b2f75..1bea76b 100644 --- a/testing/testing-setup/references/android/develop/ui/compose/testing/common-patterns.md +++ b/testing/testing-setup/references/android/develop/ui/compose/testing/common-patterns.md @@ -4,15 +4,24 @@ You can test your Compose app with well-established approaches and patterns. ### Test in isolation -[`ComposeTestRule`](https://developer.android.com/reference/kotlin/androidx/compose/ui/test/junit4/ComposeTestRule) lets you start an activity displaying any composable: your full application, a single screen, or a small element. It's also a good practice to check that your composables are correctly encapsulated and they work independently, allowing for easier and more focused UI testing. +[`ComposeTestRule`](https://developer.android.com/reference/kotlin/androidx/compose/ui/test/junit4/ComposeTestRule) lets you start an activity displaying any composable: +your full application, a single screen, or a small element. It's also a good +practice to check that your composables are correctly encapsulated and they work +independently, allowing for easier and more focused UI testing. -This doesn't mean you should *only* create unit UI tests. UI tests scoping larger parts of your UI are also very important. +This doesn't mean you should *only* create unit UI tests. UI tests scoping +larger parts of your UI are also very important. ### Access the activity and resources after setting your own content -Oftentimes you need to set the content under test using `composeTestRule.setContent` and you also need to access activity resources, for example to assert that a displayed text matches a string resource. However, you can't call `setContent` on a rule created with `createAndroidComposeRule()` if the activity already calls it. +Oftentimes you need to set the content under test using +`composeTestRule.setContent` and you also need to access activity resources, for +example to assert that a displayed text matches a string resource. However, you +can't call `setContent` on a rule created with `createAndroidComposeRule()` if +the activity already calls it. -A common pattern to achieve this is to create an `AndroidComposeTestRule` using an empty activity such as [`ComponentActivity`](https://developer.android.com/reference/androidx/activity/ComponentActivity). +A common pattern to achieve this is to create an `AndroidComposeTestRule` using +an empty activity such as [`ComponentActivity`](https://developer.android.com/reference/androidx/activity/ComponentActivity). class MyComposeTest { @@ -32,13 +41,17 @@ A common pattern to achieve this is to create an `AndroidComposeTestRule` using } } -Note that `ComponentActivity` needs to be added to your app's `AndroidManifest.xml` file. Enable that by adding this dependency to your module: +Note that `ComponentActivity` needs to be added to your app's +`AndroidManifest.xml` file. Enable that by adding this dependency to your +module: debugImplementation("androidx.compose.ui:ui-test-manifest:$compose_version") ### Custom semantics properties -You can create custom [semantics](https://developer.android.com/develop/ui/compose/testing/semantics) properties to expose information to tests. To do this, define a new `SemanticsPropertyKey` and make it available using the `SemanticsPropertyReceiver`. +You can create custom [semantics](https://developer.android.com/develop/ui/compose/testing/semantics) properties to expose information to tests. +To do this, define a new `SemanticsPropertyKey` and make it available using the +`SemanticsPropertyReceiver`. // Creates a semantics property of type Long. val PickedDateKey = SemanticsPropertyKey("PickedDate") @@ -51,7 +64,8 @@ Now use that property in the `semantics` modifier: modifier = Modifier.semantics { pickedDate = datePickerValue } ) -From tests, use `SemanticsMatcher.expectValue` to assert the value of the property: +From tests, use `SemanticsMatcher.expectValue` to assert the value of the +property: composeTestRule .onNode(SemanticsMatcher.expectValue(PickedDateKey, 1445378400)) // 2015-10-21 @@ -62,9 +76,12 @@ From tests, use `SemanticsMatcher.expectValue` to assert the value of the proper ### Verify state restoration -Verify that the state of your Compose elements is correctly restored when the activity or process is recreated. Perform such checks without relying on activity recreation with the [`StateRestorationTester`](https://developer.android.com/reference/kotlin/androidx/compose/ui/test/junit4/StateRestorationTester) class. +Verify that the state of your Compose elements is correctly restored when the +activity or process is recreated. Perform such checks without relying on +activity recreation with the [`StateRestorationTester`](https://developer.android.com/reference/kotlin/androidx/compose/ui/test/junit4/StateRestorationTester) class. -This class lets you simulate the recreation of a composable. It's especially useful to verify the implementation of [`rememberSaveable`](https://developer.android.com/reference/kotlin/androidx/compose/runtime/saveable/rememberSaveable.composable#rememberSaveable(kotlin.Array,androidx.compose.runtime.saveable.Saver,kotlin.String,kotlin.Function0)). +This class lets you simulate the recreation of a composable. It's especially +useful to verify the implementation of [`rememberSaveable`](https://developer.android.com/reference/kotlin/androidx/compose/runtime/saveable/rememberSaveable.composable#rememberSaveable(kotlin.Array,androidx.compose.runtime.saveable.Saver,kotlin.String,kotlin.Function0)). class MyStateRestorationTests { @@ -89,25 +106,40 @@ This class lets you simulate the recreation of a composable. It's especially use ### Test different device configurations -Android apps need to adapt to many changing conditions: window sizes, locales, font sizes, dark and light themes, and more. Most of these conditions are derived from device-level values controlled by the user and exposed with the current [`Configuration`](https://developer.android.com/reference/android/content/res/Configuration) instance. Testing different configurations directly in a test is difficult since the test must configure device-level properties. +Android apps need to adapt to many changing conditions: window sizes, locales, +font sizes, dark and light themes, and more. Most of these conditions are +derived from device-level values controlled by the user and exposed with the +current [`Configuration`](https://developer.android.com/reference/android/content/res/Configuration) instance. Testing different configurations +directly in a test is difficult since the test must configure device-level +properties. -[`DeviceConfigurationOverride`](https://developer.android.com/reference/kotlin/androidx/compose/ui/test/DeviceConfigurationOverride) is a test-only API that lets you simulate different device configurations in a localized way for the `@Composable` content under test. +[`DeviceConfigurationOverride`](https://developer.android.com/reference/kotlin/androidx/compose/ui/test/DeviceConfigurationOverride) is a test-only API that lets you simulate +different device configurations in a localized way for the `@Composable` content +under test. -The companion object of `DeviceConfigurationOverride` has the following extension functions, which override device-level configuration properties: +The companion object of `DeviceConfigurationOverride` has the following +extension functions, which override device-level configuration properties: - [`DeviceConfigurationOverride.DarkMode()`](https://developer.android.com/reference/kotlin/androidx/compose/ui/test/DeviceConfigurationOverride.Companion#(androidx.compose.ui.test.DeviceConfigurationOverride.Companion).DarkMode(kotlin.Boolean)): Overrides the system to dark theme or light theme. -- [`DeviceConfigurationOverride.FontScale()`](https://developer.android.com/reference/kotlin/androidx/compose/ui/test/DeviceConfigurationOverride.Companion#(androidx.compose.ui.test.DeviceConfigurationOverride.Companion).FontScale(kotlin.Float)): Overrides the [system font scale](https://developer.android.com/training/multiscreen/screendensities#TaskUseDP). +- [`DeviceConfigurationOverride.FontScale()`](https://developer.android.com/reference/kotlin/androidx/compose/ui/test/DeviceConfigurationOverride.Companion#(androidx.compose.ui.test.DeviceConfigurationOverride.Companion).FontScale(kotlin.Float)): Overrides the [system font + scale](https://developer.android.com/training/multiscreen/screendensities#TaskUseDP). - [`DeviceConfigurationOverride.FontWeightAdjustment()`](https://developer.android.com/reference/kotlin/androidx/compose/ui/test/DeviceConfigurationOverride.Companion#(androidx.compose.ui.test.DeviceConfigurationOverride.Companion).FontWeightAdjustment(kotlin.Int)): Overrides the system font weight adjustment. - [`DeviceConfigurationOverride.ForcedSize()`](https://developer.android.com/reference/kotlin/androidx/compose/ui/test/DeviceConfigurationOverride.Companion#(androidx.compose.ui.test.DeviceConfigurationOverride.Companion).ForcedSize(androidx.compose.ui.unit.DpSize)): Forces a specific amount of space regardless of device size. -- [`DeviceConfigurationOverride.LayoutDirection()`](https://developer.android.com/reference/kotlin/androidx/compose/ui/test/DeviceConfigurationOverride.Companion#(androidx.compose.ui.test.DeviceConfigurationOverride.Companion).LayoutDirection(androidx.compose.ui.unit.LayoutDirection)): Overrides the [layout direction](https://developer.android.com/training/basics/supporting-devices/languages#SupportLayoutMirroring) (left-to-right or right-to-left). +- [`DeviceConfigurationOverride.LayoutDirection()`](https://developer.android.com/reference/kotlin/androidx/compose/ui/test/DeviceConfigurationOverride.Companion#(androidx.compose.ui.test.DeviceConfigurationOverride.Companion).LayoutDirection(androidx.compose.ui.unit.LayoutDirection)): Overrides the [layout + direction](https://developer.android.com/training/basics/supporting-devices/languages#SupportLayoutMirroring) (left-to-right or right-to-left). - [`DeviceConfigurationOverride.Locales()`](https://developer.android.com/reference/kotlin/androidx/compose/ui/test/DeviceConfigurationOverride.Companion#(androidx.compose.ui.test.DeviceConfigurationOverride.Companion).Locales(androidx.compose.ui.text.intl.LocaleList)): Overrides the [locale](https://developer.android.com/guide/topics/resources/localization). - [`DeviceConfigurationOverride.RoundScreen()`](https://developer.android.com/reference/kotlin/androidx/compose/ui/test/DeviceConfigurationOverride.Companion#(androidx.compose.ui.test.DeviceConfigurationOverride.Companion).RoundScreen(kotlin.Boolean)): Overrides if the screen is [round](https://developer.android.com/design/ui/wear/guides/foundations/getting-started#design-for-round). -To apply a specific override, wrap the content under test in a call to the [`DeviceConfigurationOverride()`](https://developer.android.com/reference/kotlin/androidx/compose/ui/test/DeviceConfigurationOverride.composable#DeviceConfigurationOverride(androidx.compose.ui.test.DeviceConfigurationOverride,kotlin.Function0)) top-level function, passing the override to apply as a parameter. +To apply a specific override, wrap the content under test in a call to the +[`DeviceConfigurationOverride()`](https://developer.android.com/reference/kotlin/androidx/compose/ui/test/DeviceConfigurationOverride.composable#DeviceConfigurationOverride(androidx.compose.ui.test.DeviceConfigurationOverride,kotlin.Function0)) top-level function, passing the override +to apply as a parameter. -For example, the following code applies the `DeviceConfigurationOverride.ForcedSize()` override to change the density locally, forcing the `MyScreen` composable to be rendered in a large landscape window, even if the device the test is running on doesn't support that window size directly: +For example, the following code applies the +`DeviceConfigurationOverride.ForcedSize()` override to change the density +locally, forcing the `MyScreen` composable to be rendered in a large landscape +window, even if the device the test is running on doesn't support that window +size directly: -
```kotlin composeTestRule.setContent { @@ -117,14 +149,13 @@ composeTestRule.setContent { MyScreen() // Will be rendered in the space for 1280dp by 800dp without clipping. } } - ```
-To apply multiple overrides together, use [`DeviceConfigurationOverride.then()`](https://developer.android.com/reference/kotlin/androidx/compose/ui/test/DeviceConfigurationOverride#(androidx.compose.ui.test.DeviceConfigurationOverride).then(androidx.compose.ui.test.DeviceConfigurationOverride)): +To apply multiple overrides together, use +[`DeviceConfigurationOverride.then()`](https://developer.android.com/reference/kotlin/androidx/compose/ui/test/DeviceConfigurationOverride#(androidx.compose.ui.test.DeviceConfigurationOverride).then(androidx.compose.ui.test.DeviceConfigurationOverride)): -
```kotlin composeTestRule.setContent { @@ -135,7 +166,6 @@ composeTestRule.setContent { Text(text = "text with increased scale and weight") } } - ```
diff --git a/testing/testing-setup/references/android/studio/preview/compose-screenshot-testing.md b/testing/testing-setup/references/android/studio/preview/compose-screenshot-testing.md index 391510e..b1f7876 100644 --- a/testing/testing-setup/references/android/studio/preview/compose-screenshot-testing.md +++ b/testing/testing-setup/references/android/studio/preview/compose-screenshot-testing.md @@ -1,9 +1,16 @@ > [!WARNING] > **Experimental:** Compose Preview Screenshot Testing is still in development. Its features and APIs are subject to change substantially during the alpha phase. Report any feedback and issues through the [issue tracker](https://issuetracker.google.com/issues/new?component=192708&template=840533). -Screenshot testing is an effective way to verify how your UI looks to users. The Compose Preview Screenshot Testing tool combines the simplicity and features of [composable previews](https://developer.android.com/develop/ui/compose/tooling/previews) with the productivity gains of running host-side screenshot tests. Compose Preview Screenshot Testing is designed to be as straightforward to use as composable previews. +Screenshot testing is an effective way to verify how your UI looks to users. +The Compose Preview Screenshot Testing tool combines the simplicity and +features of [composable previews](https://developer.android.com/develop/ui/compose/tooling/previews) with the productivity +gains of running host-side screenshot tests. Compose Preview Screenshot Testing +is designed to be as straightforward to use as composable previews. -A screenshot test is an automated test that takes a screenshot of a piece of UI and then compares it against a previously approved reference image. If the images don't match, the test fails and produces an HTML report to help you compare and find the differences. +A screenshot test is an automated test that takes a screenshot of a piece of UI +and then compares it against a previously approved reference image. If the +images don't match, the test fails and produces an HTML report to help you +compare and find the differences. With the Compose Preview Screenshot Testing tool, you can: @@ -17,30 +24,31 @@ With the Compose Preview Screenshot Testing tool, you can: ## IDE integration -While you can use the Compose Preview Screenshot Testing tool by running the underlying Gradle tasks (`updateScreenshotTest` and `validateScreenshotTest`) manually, Android Studio Otter 3 Feature Drop Canary 4 introduces a full IDE integration. This lets you generate reference images, run tests, and analyze validation failures entirely within the IDE. Here are some of the key features: +While you can use the Compose Preview Screenshot Testing tool by running the +underlying Gradle tasks (`updateScreenshotTest` and `validateScreenshotTest`) +manually, Android Studio Otter 3 Feature Drop Canary 4 introduces a full IDE +integration. This lets you generate reference images, run tests, and analyze +validation failures entirely within the IDE. Here are some of the key features: - **In-editor gutter icons.** You can now run tests or update reference images directly from the source code. Green run icons appear in the gutter next to composables and classes annotated with `@PreviewTest`. - **Run screenshot tests.** Execute tests specifically for a single function or for an entire class. - **Add or update reference images.** Trigger the update flow specifically for the selected scope. - - **Interactive reference management.** Updating reference images is now safer and more granular. - **New reference image generation dialog.** Instead of running a bulk Gradle task, a new dialog lets you visualize and select exactly which previews to generate or update. - **Preview variations.** The dialog lists all preview variations (such as light theme or dark theme, or different devices) individually, allowing you to select or clear specific items before generating images. - - **Integrated test results and diff viewer.** View results without leaving the IDE. - **Unified run panel.** Screenshot test results appear in the standard **Run** tool window. Tests are grouped by class and function, with pass or fail status clearly marked. - **Visual diff tool.** When a test fails, the **Screenshot** tab lets you compare the *Reference* , *Actual* , and *Diff* images side-by-side. - **Detailed attributes.** An **Attributes** tab provides metadata on failed tests, including match percentage, image dimensions, and the specific preview configuration used (for example, `uiMode` or `fontScale`). - - **Flexible test scoping.** You can now execute screenshot tests with various scopes directly from the Project View. Right-click a module, directory, file, or class to run screenshot tests specifically for that selection. - ## Requirements -To use Compose Preview Screenshot Testing through the full IDE integration, your project must meet the following requirements: +To use Compose Preview Screenshot Testing through the full IDE integration, your +project must meet the following requirements: - Android Studio Panda 1 Canary 4 or higher. - Android Gradle Plugin (AGP) version 9.0 or higher. @@ -49,7 +57,8 @@ To use Compose Preview Screenshot Testing through the full IDE integration, your - JDK version 17 or higher. - Compose enabled for your project. We recommend enabling Compose using the [Compose Compiler Gradle plugin](https://developer.android.com/develop/ui/compose/compiler). -If you only want to use the underlying Gradle tasks without the IDE integration, the requirements are as follows: +If you only want to use the underlying Gradle tasks without the IDE integration, +the requirements are as follows: - Android Gradle Plugin (AGP) version 8.5.0 or higher. - Compose Preview Screenshot Testing plugin version [0.0.1-alpha15](https://developer.android.com/studio/preview/compose-screenshot-testing-release-notes#alpha15) or higher. @@ -62,19 +71,22 @@ If you only want to use the underlying Gradle tasks without the IDE integration, ## Setup -Both the integrated tool and the underlying Gradle tasks rely on the Compose Preview Screenshot Testing plugin. To set up the plugin, follow these steps: +Both the integrated tool and the underlying Gradle tasks rely on the Compose +Preview Screenshot Testing plugin. To set up the plugin, follow these steps: 1. Enable the experimental property in your project's `gradle.properties` file. android.experimental.enableScreenshotTest=true -2. In the `android {}` block of your module-level `build.gradle.kts` file, enable the experimental flag to use the `screenshotTest` source set. +2. In the `android {}` block of your module-level `build.gradle.kts` file, + enable the experimental flag to use the `screenshotTest` source set. android { experimentalProperties["android.experimental.enableScreenshotTest"] = true } -3. Add the `com.android.compose.screenshot` plugin, version `0.0.1-alpha15` to your project. +3. Add the `com.android.compose.screenshot` plugin, version `0.0.1-alpha15` to + your project. 1. Add the plugin to your version catalogs file: @@ -86,13 +98,16 @@ Both the integrated tool and the underlying Gradle tasks rely on the Compose Pre [plugins] screenshot = { id = "com.android.compose.screenshot", version.ref = "screenshot"} - 2. In your module-level `build.gradle.kts` file, add the plugin in the `plugins {}` block: + 2. In your module-level `build.gradle.kts` file, add the plugin in the + `plugins {}` block: plugins { alias(libs.plugins.screenshot) } -4. Add the [`screenshot-validation-api`](https://maven.google.com/web/index.html?q=screenshot-validation-api#com.android.tools.screenshot:screenshot-validation-api) and [`ui-tooling`](https://maven.google.com/web/index.html?q=tooling#androidx.compose.ui:ui-tooling) dependencies. +4. Add the [`screenshot-validation-api`](https://maven.google.com/web/index.html?q=screenshot-validation-api#com.android.tools.screenshot:screenshot-validation-api) + and [`ui-tooling`](https://maven.google.com/web/index.html?q=tooling#androidx.compose.ui:ui-tooling) + dependencies. 1. Add them to your version catalogs: @@ -109,11 +124,15 @@ Both the integrated tool and the underlying Gradle tasks rely on the Compose Pre ## Designate composable previews to use for screenshot tests -To designate the composable previews you want to use for screenshot tests, mark the previews with the `@PreviewTest` annotation. The previews must be located in the new `screenshotTest` source set, for example: +To designate the composable previews you want to use for screenshot tests, mark +the previews with the `@PreviewTest` annotation. The previews must be located in +the new `screenshotTest` source set, for example: -`app/src/screenshotTest/kotlin/com/example/yourapp/` `ExamplePreviewScreenshotTest.kt` +`app/src/screenshotTest/kotlin/com/example/yourapp/` +`ExamplePreviewScreenshotTest.kt` -You can add more composables or previews, including multi-previews, in this file or other files created in the same source set. +You can add more composables or previews, including multi-previews, in +this file or other files created in the same source set. package com.example.yourapp @@ -133,11 +152,16 @@ You can add more composables or previews, including multi-previews, in this file ## Generate reference images -After you set up a test class, you need to generate reference images for each preview. These reference images are used to identify changes later, after you make code changes. To generate reference images for your composable preview screenshot tests, follow the instructions in this section for the IDE integration or for the Gradle tasks. +After you set up a test class, you need to generate reference images for each +preview. These reference images are used to identify changes later, after you +make code changes. To generate reference images for your composable preview +screenshot tests, follow the instructions in this section for the IDE +integration or for the Gradle tasks. ### In the IDE -Click the gutter icon next to a `@PreviewTest` function and select **Add/Update Reference Images** . Select the previews in the dialog and click **Add**. +Click the gutter icon next to a `@PreviewTest` function and select **Add/Update +Reference Images** . Select the previews in the dialog and click **Add**. ### With the Gradle tasks @@ -146,36 +170,46 @@ Run the following Gradle task: - Linux and macOS: `./gradlew updateDebugScreenshotTest` (`./gradlew :{module}:update{Variant}ScreenshotTest`) - Windows: `gradlew updateDebugScreenshotTest` (`gradlew :{module}:update{Variant}ScreenshotTest`) -After the task completes, find the reference images in `app/src/screenshotTestDebug/reference` (`{module}/src/screenshotTest{Variant}/reference`). +After the task completes, find the reference images in +`app/src/screenshotTestDebug/reference` +(`{module}/src/screenshotTest{Variant}/reference`). > [!NOTE] > **Note:** The reference images are named with a concatenation of the fully-qualified name of the test function and a hash of the preview parameters, for example `com.sample.screenshottests.test1_da39a3ee_c2200e98_0.png`. ## Generate a test report -Once the reference images exist, generate a test report by following the instructions in this section for the IDE integration or for the Gradle tasks. +Once the reference images exist, generate a test report by following the +instructions in this section for the IDE integration or for the Gradle tasks. ### In the IDE -Click the gutter icon next to a `@PreviewTest` function and select **Run 'ScreenshotTests'**. +Click the gutter icon next to a `@PreviewTest` function and select **Run +'ScreenshotTests'**. -If a test fails, click the test name in the **Run** panel. Select the **Screenshot** tab to inspect the image diff using the integrated zoom and pan controls. +If a test fails, click the test name in the **Run** panel. Select the +**Screenshot** tab to inspect the image diff using the integrated zoom and pan +controls. > [!NOTE] > **Note:** Renaming a function annotated with `@PreviewTest` breaks the association with existing reference images. In that case, you must [regenerate reference images](https://developer.android.com/studio/preview/compose-screenshot-testing#generate-reference-images) for the new function name. ### With the Gradle tasks -Run the validate task to take a new screenshot and compare it with the reference image: +Run the validate task to take a new screenshot and compare it with the +reference image: - Linux and macOS: `./gradlew validateDebugScreenshotTest` (`./gradlew :{module}:validate{Variant}ScreenshotTest`) - Windows: `gradlew validateDebugScreenshotTest` (`gradlew :{module}:validate{Variant}ScreenshotTest`) -The verification task creates an HTML report at `{module}/build/reports/screenshotTest/preview/{variant}/index.html`. +The verification task creates an HTML report at +`{module}/build/reports/screenshotTest/preview/{variant}/index.html`. ## Troubleshooting -Compose Preview Screenshot Testing runs host-side tests, which can be memory-intensive. You can increase the maximum heap size for the test JVM by adding the following property to your `gradle.properties` file: +Compose Preview Screenshot Testing runs host-side tests, which can be +memory-intensive. You can increase the maximum heap size for the test JVM by +adding the following property to your `gradle.properties` file: android.compose.screenshot.maxHeapSize=4g @@ -183,8 +217,11 @@ Compose Preview Screenshot Testing runs host-side tests, which can be memory-int - **Kotlin Multiplatform (KMP):** Both the IDE and the underlying plugin are engineered exclusively for Android projects. They don't support non-Android targets in KMP projects. -You can find the complete list of current known issues in the tool's [issue tracker component](https://issuetracker.google.com/issues?q=status:open+componentid:1581441&s=created_time:desc). Report any other feedback and issues through the [issue tracker](https://issuetracker.google.com/issues/new?component=192708&template=840533). +You can find the complete list of current known issues in the tool's +[issue tracker component](https://issuetracker.google.com/issues?q=status:open+componentid:1581441&s=created_time:desc). Report any other feedback and issues +through the [issue tracker](https://issuetracker.google.com/issues/new?component=192708&template=840533). ## Release updates -For a full list of release updates, see the [release notes](https://developer.android.com/studio/preview/compose-screenshot-testing-release-notes). \ No newline at end of file +For a full list of release updates, see the +[release notes](https://developer.android.com/studio/preview/compose-screenshot-testing-release-notes). \ No newline at end of file diff --git a/testing/testing-setup/references/android/training/dependency-injection/hilt-testing.md b/testing/testing-setup/references/android/training/dependency-injection/hilt-testing.md index e1fe4ac..022bf07 100644 --- a/testing/testing-setup/references/android/training/dependency-injection/hilt-testing.md +++ b/testing/testing-setup/references/android/training/dependency-injection/hilt-testing.md @@ -1,8 +1,12 @@ -One of the benefits of using dependency injection frameworks like Hilt is that it makes testing your code easier. +One of the benefits of using dependency injection frameworks like Hilt is that +it makes testing your code easier. ## Unit tests -Hilt isn't necessary for unit tests, since when testing a class that uses constructor injection, you don't need to use Hilt to instantiate that class. Instead, you can directly call a class constructor by passing in fake or mock dependencies, just as you would if the constructor weren't annotated: +Hilt isn't necessary for unit tests, since when testing a class that uses +constructor injection, you don't need to use Hilt to instantiate that class. +Instead, you can directly call a class constructor by passing in fake or mock +dependencies, just as you would if the constructor weren't annotated: ```kotlin @ActivityScoped @@ -22,15 +26,21 @@ class AnalyticsAdapterTest { } ``` -The same applies to ViewModel classes obtained by calling `hiltViewModel()` in your composables. In unit tests, construct the ViewModel directly with fakes. For information on how state flows from a ViewModel into composables, see [State and Jetpack Compose](https://developer.android.com/develop/ui/compose/state) and [Where to hoist state](https://developer.android.com/develop/ui/compose/state-hoisting). +The same applies to ViewModel classes obtained by calling `hiltViewModel()` in +your composables. In unit tests, construct the ViewModel directly with fakes. +For information on how state flows from a ViewModel into composables, see +[State and Jetpack Compose](https://developer.android.com/develop/ui/compose/state) and [Where to hoist state](https://developer.android.com/develop/ui/compose/state-hoisting). ## End-to-end tests -For integration tests, Hilt injects dependencies as it would in your production code. Testing with Hilt requires no maintenance because Hilt automatically generates a new set of components for each test. +For integration tests, Hilt injects dependencies as it would in your production +code. Testing with Hilt requires no maintenance because Hilt automatically +generates a new set of components for each test. ### Adding testing dependencies -To use Hilt in your tests, include the `hilt-android-testing` dependency in your project: +To use Hilt in your tests, include the `hilt-android-testing` dependency in your +project: ```kotlin dependencies { @@ -54,9 +64,11 @@ dependencies { ### UI test setup -You must annotate any UI test that uses Hilt with `@HiltAndroidTest`. This annotation is responsible for generating the Hilt components for each test. +You must annotate any UI test that uses Hilt with `@HiltAndroidTest`. This +annotation is responsible for generating the Hilt components for each test. -Also, you need to add the `HiltAndroidRule` to the test class. It manages the components' state and is used to perform injection on your test: +Also, you need to add the `HiltAndroidRule` to the test class. It manages the +components' state and is used to perform injection on your test: ```kotlin @HiltAndroidTest @@ -73,21 +85,36 @@ class SettingsScreenTest { ``` > [!NOTE] -> **Note:** If you have other rules in your test, see [Multiple TestRule objects in your instrumented test](https://developer.android.com/training/dependency-injection/hilt-testing#multiple-testrules). +> **Note:** If you have other rules in your test, see [Multiple TestRule objects in +> your instrumented test](https://developer.android.com/training/dependency-injection/hilt-testing#multiple-testrules). -Next, your test needs to know about the `Application` class that Hilt automatically generates for you. +Next, your test needs to know about the `Application` class that Hilt +automatically generates for you. -To let Hilt inject dependencies, you must create an empty activity named `HiltTestActivity` in your `androidTest` source set and annotate it with `@AndroidEntryPoint`. `createAndroidComposeRule` then uses this activity as the host for your composable content. +To let Hilt inject dependencies, you must create an empty activity named +`HiltTestActivity` in your `androidTest` source set and annotate it with +`@AndroidEntryPoint`. `createAndroidComposeRule` then uses this activity as the +host for your composable content. #### Test application -You must execute instrumented tests that use Hilt in an `Application` object that supports Hilt. The library provides `HiltTestApplication` for use in tests. If your tests need a different base application, see [Custom application for tests](https://developer.android.com/training/dependency-injection/hilt-testing#custom-application). +You must execute instrumented tests that use Hilt in an `Application` object +that supports Hilt. The library provides `HiltTestApplication` for use in tests. +If your tests need a different base application, see [Custom application for +tests](https://developer.android.com/training/dependency-injection/hilt-testing#custom-application). -You must set your test application to run in your [instrumented tests](https://developer.android.com/training/testing/ui-testing) or [Robolectric tests](http://robolectric.org/). The following instructions aren't specific to Hilt, but are general guidelines on how to specify a custom application to run in tests. +You must set your test application to run in your [instrumented +tests](https://developer.android.com/training/testing/ui-testing) or [Robolectric +tests](http://robolectric.org/). The following instructions aren't +specific to Hilt, but are general guidelines on how to specify a custom +application to run in tests. ##### Set the test application in instrumented tests -To use the Hilt test application in [instrumented tests](https://developer.android.com/training/testing/ui-testing), you need to configure a new test runner. This makes Hilt work for all of the instrumented tests in your project. Perform the following steps: +To use the Hilt test application in [instrumented +tests](https://developer.android.com/training/testing/ui-testing), you need to configure a new test runner. +This makes Hilt work for all of the instrumented tests in your project. Perform +the following steps: 1. Create a custom class that extends [`AndroidJUnitRunner`](https://developer.android.com/reference/kotlin/androidx/test/runner/AndroidJUnitRunner) in the `androidTest` folder. 2. Override the `newApplication` function and pass in the name of the generated Hilt test application. @@ -102,7 +129,10 @@ class CustomTestRunner : AndroidJUnitRunner() { } ``` -Next, configure this test runner in your Gradle file as described in the [instrumented unit test guide](https://developer.android.com/training/testing/unit-testing/instrumented-unit-tests#setup). Make sure you use the full classpath: +Next, configure this test runner in your Gradle file as described in the +[instrumented unit test +guide](https://developer.android.com/training/testing/unit-testing/instrumented-unit-tests#setup). Make sure +you use the full classpath: ```kotlin android { @@ -115,11 +145,13 @@ android { ##### Set the test application in Robolectric tests -If you use Robolectric to test your UI layer, you can specify which application to use in the `robolectric.properties` file: +If you use Robolectric to test your UI layer, you can specify which application +to use in the `robolectric.properties` file: `application = dagger.hilt.android.testing.HiltTestApplication` -Alternatively, you can configure the application on each test individually by using Robolectric's `@Config` annotation: +Alternatively, you can configure the application on each test individually by +using Robolectric's `@Config` annotation: ```kotlin @HiltAndroidTest @@ -135,11 +167,13 @@ class SettingsScreenTest { ### Testing features -Once Hilt is ready to use in your tests, you can use several features to customize the testing process. +Once Hilt is ready to use in your tests, you can use several features to +customize the testing process. #### Inject types in tests -To inject types into a test, use `@Inject` for field injection. To tell Hilt to populate the `@Inject` fields, call `hiltRule.inject()`. +To inject types into a test, use `@Inject` for field injection. To tell Hilt to +populate the `@Inject` fields, call `hiltRule.inject()`. See the following example of an instrumented test: @@ -174,9 +208,14 @@ class SettingsScreenTest { #### Replace a binding -If you need to inject a fake or mock instance of a dependency, you need to tell Hilt not to use the binding that it used in production code and to use a different one instead. To replace a binding, you need to replace the module that contains the binding with a test module that contains the bindings that you want to use in the test. +If you need to inject a fake or mock instance of a dependency, you need to tell +Hilt not to use the binding that it used in production code and to use a +different one instead. To replace a binding, you need to replace the module that +contains the binding with a test module that contains the bindings that you want +to use in the test. -For example, suppose your production code declares a binding for `AnalyticsService` as follows: +For example, suppose your production code declares a binding for +`AnalyticsService` as follows: ```kotlin @Module @@ -191,7 +230,10 @@ abstract class AnalyticsModule { } ``` -To replace the `AnalyticsService` binding in tests, create a new Hilt module in the `test` or `androidTest` folder with the fake dependency and annotate it with `@TestInstallIn`. All the tests in that folder are injected with the fake dependency instead. +To replace the `AnalyticsService` binding in tests, create a new Hilt module in +the `test` or `androidTest` folder with the fake dependency and annotate it +with `@TestInstallIn`. All the tests in that folder are injected with the fake +dependency instead. ```kotlin @Module @@ -209,13 +251,19 @@ abstract class FakeAnalyticsModule { } ``` -Because composables typically consume these dependencies indirectly through a ViewModel obtained with `hiltViewModel()`, replacing the binding in Hilt is enough. The composable under test picks up the fake automatically. +Because composables typically consume these dependencies indirectly through a +ViewModel obtained with `hiltViewModel()`, replacing the binding in Hilt is +enough. The composable under test picks up the fake automatically. #### Replace a binding in a single test -To replace a binding in a single test instead of all tests, uninstall a Hilt module from a test using the `@UninstallModules` annotation and create a new test module inside the test. +To replace a binding in a single test instead of all tests, uninstall a Hilt +module from a test using the `@UninstallModules` annotation and create a new +test module inside the test. -Following the `AnalyticsService` example from the previous version, begin by telling Hilt to ignore the production module by using the `@UninstallModules` annotation in the test class: +Following the `AnalyticsService` example from the previous version, begin by +telling Hilt to ignore the production module by using the `@UninstallModules` +annotation in the test class: ```kotlin @UninstallModules(AnalyticsModule::class) @@ -223,7 +271,8 @@ Following the `AnalyticsService` example from the previous version, begin by tel class SettingsScreenTest { ... } ``` -Next, you must replace the binding. Create a new module within the test class that defines the test binding: +Next, you must replace the binding. Create a new module within the test class +that defines the test binding: ```kotlin @UninstallModules(AnalyticsModule::class) @@ -245,7 +294,11 @@ class SettingsScreenTest { } ``` -This only replaces the binding for a single test class. If you want to replace the binding for all test classes, use the `@TestInstallIn` annotation from the section above. Alternatively, you can put the test binding in the `test` module for Robolectric tests, or in the `androidTest` module for instrumented tests. The recommendation is to use `@TestInstallIn` whenever possible. +This only replaces the binding for a single test class. If you want to replace +the binding for all test classes, use the `@TestInstallIn` annotation from the +section above. Alternatively, you can put the test binding in the `test` module +for Robolectric tests, or in the `androidTest` module for instrumented tests. +The recommendation is to use `@TestInstallIn` whenever possible. > [!WARNING] > **Warning:** You cannot uninstall modules that are not annotated with `@InstallIn`. Attempting to do so causes a compilation error. @@ -258,9 +311,12 @@ This only replaces the binding for a single test class. If you want to replace t #### Binding new values -Use the `@BindValue` annotation to easily bind fields in your test into the Hilt dependency graph. Annotate a field with `@BindValue` and it will be bound under the declared field type with any qualifiers that are present for that field. +Use the `@BindValue` annotation to easily bind fields in your test into the Hilt +dependency graph. Annotate a field with `@BindValue` and it will be bound under +the declared field type with any qualifiers that are present for that field. -In the `AnalyticsService` example, you can replace `AnalyticsService` with a fake by using `@BindValue`: +In the `AnalyticsService` example, you can replace `AnalyticsService` with a +fake by using `@BindValue`: ```kotlin @UninstallModules(AnalyticsModule::class) @@ -274,9 +330,13 @@ class SettingsScreenTest { } ``` -This simplifies both replacing a binding and referencing a binding in your test by allowing you to do both at the same time. +This simplifies both replacing a binding and referencing a binding in your test +by allowing you to do both at the same time. -`@BindValue` works with qualifiers and other testing annotations. For example, if you use testing libraries such as [Mockito](https://site.mockito.org/), you could use it in a Robolectric test as follows: +`@BindValue` works with qualifiers and other testing annotations. For example, +if you use testing libraries such as +[Mockito](https://site.mockito.org/), you could use it in a +Robolectric test as follows: ```kotlin ... @@ -290,7 +350,10 @@ class SettingsScreenTest { } ``` -If you need to add a [multibinding](https://dagger.dev/dev-guide/multibindings), you can use the `@BindValueIntoSet` and `@BindValueIntoMap` annotations in place of `@BindValue`. `@BindValueIntoMap` requires you to also annotate the field with a map key annotation. +If you need to add a [multibinding](https://dagger.dev/dev-guide/multibindings), +you can use the `@BindValueIntoSet` and `@BindValueIntoMap` annotations in place +of `@BindValue`. `@BindValueIntoMap` requires you to also annotate the field +with a map key annotation. ## Special cases @@ -298,23 +361,36 @@ Hilt also provides features to support nonstandard use cases. ### Custom application for tests -If you cannot use `HiltTestApplication` because your test application needs to extend another application, annotate a new class or interface with `@CustomTestApplication`, passing in the value of the base class you want the generated Hilt application to extend. +If you cannot use `HiltTestApplication` because your test application needs to +extend another application, annotate a new class or interface with +`@CustomTestApplication`, passing in the value of the base class you want the +generated Hilt application to extend. -`@CustomTestApplication` will generate an `Application` class ready for testing with Hilt that extends the application you passed as a parameter. +`@CustomTestApplication` will generate an `Application` class ready for testing +with Hilt that extends the application you passed as a parameter. ```kotlin @CustomTestApplication(BaseApplication::class) interface HiltTestApplication ``` -In the example, Hilt generates an `Application` named `HiltTestApplication_Application` that extends the `BaseApplication` class. In general, the name of the generated application is the name of the annotated class appended with `_Application`. You must set the generated Hilt test application to run in your [instrumented tests](https://developer.android.com/training/testing/ui-testing) or [Robolectric tests](http://robolectric.org/) as described in [Test application](https://developer.android.com/training/dependency-injection/hilt-testing#test-application). +In the example, Hilt generates an `Application` named +`HiltTestApplication_Application` that extends the `BaseApplication` class. In +general, the name of the generated application is the name of the annotated +class appended with `_Application`. You must set the generated Hilt test +application to run in your [instrumented tests](https://developer.android.com/training/testing/ui-testing) or +[Robolectric tests](http://robolectric.org/) as described in [Test +application](https://developer.android.com/training/dependency-injection/hilt-testing#test-application). > [!NOTE] > **Note:** Because `HiltTestApplication_Application` is code that Hilt generates at runtime, the IDE might highlight it in red until you run your tests. ### Multiple TestRule objects in your instrumented test -Compose UI tests already combine `HiltAndroidRule` with a Compose test rule such as `createAndroidComposeRule`. If you have additional `TestRule` objects, make sure `HiltAndroidRule` runs first. Declare the execution order with the `order` attribute on `@Rule`: +Compose UI tests already combine `HiltAndroidRule` with a Compose test rule +such as `createAndroidComposeRule`. If you have additional `TestRule` objects, +make sure `HiltAndroidRule` runs first. Declare the execution order with the +`order` attribute on `@Rule`: ```kotlin @HiltAndroidTest @@ -333,7 +409,8 @@ class SettingsScreenTest { } ``` -Alternatively, you can wrap the rules with `RuleChain`, placing `HiltAndroidRule` as the outer rule. +Alternatively, you can wrap the rules with `RuleChain`, placing +`HiltAndroidRule` as the outer rule. ```kotlin @HiltAndroidTest @@ -349,9 +426,12 @@ class SettingsScreenTest { ### Use an entry point before the singleton component is available -The `@EarlyEntryPoint` annotation provides an escape hatch when a Hilt entry point needs to be created before the singleton component is available in a Hilt test. +The `@EarlyEntryPoint` annotation provides an escape hatch when a Hilt entry +point needs to be created before the singleton component is available in a +Hilt test. -More information about `@EarlyEntryPoint` in the [Hilt documentation](https://dagger.dev/hilt/early-entry-point). +More information about `@EarlyEntryPoint` in the +[Hilt documentation](https://dagger.dev/hilt/early-entry-point). ## Additional resources diff --git a/tv/leanback-to-compose-tv-migration/SKILL.md b/tv/leanback-to-compose-tv-migration/SKILL.md index 0c46918..c5d709d 100644 --- a/tv/leanback-to-compose-tv-migration/SKILL.md +++ b/tv/leanback-to-compose-tv-migration/SKILL.md @@ -13,7 +13,7 @@ description: Provides instructions and architectural patterns for migrating Andr license: Complete terms in LICENSE.txt metadata: author: Google LLC - last-updated: '2026-08-06' + last-updated: '2026-08-21' keywords: - Android TV - Jetpack Compose @@ -35,7 +35,9 @@ metadata: ## The 10-foot UI -A "10-foot UI" is a design paradigm for televisions that tailors an interface for viewing from approximately 3 meters (10 feet) away. When designing for this experience, account for these key characteristics: +A "10-foot UI" is a design paradigm for televisions that tailors an interface +for viewing from approximately 3 meters (10 feet) away. When designing for this +experience, account for these key characteristics: - **Viewing distance**: Viewers are sitting far from the screen, "leaning back". Screen layouts are uncluttered, with text and UI elements that are large enough to be comfortably readable from a distance, without dense blocks of text. - **Color contrast**: To avoid washed out colors on TV displays with low contrast ratios, the design uses high-contrast palettes and distinct visual indicators so focused states remain visible across different TV panels. @@ -43,13 +45,18 @@ A "10-foot UI" is a design paradigm for televisions that tailors an interface fo ## Core architecture and library selection -When migrating an Android TV application to Jetpack Compose, you must use `androidx.tv` libraries and follow these 10-foot UI patterns: +When migrating an Android TV application to Jetpack Compose, you must use +`androidx.tv` libraries and follow these 10-foot UI patterns: - **UI modernization**: Focus on custom, cinematic layouts over legacy direct 1:1 templates. You must use Jetpack Compose for TV features like dynamic gradient hero backdrops, custom focus animations, custom navigation drawers, and custom layouts. - **Primary design system** : You must always use `androidx.tv.material3.*` (`androidx.tv:tv-material`) over mobile `androidx.compose.material3.*`. TV Material 3 provides built-in D-Pad focus handling, focus zoom scaling, and TV-optimized typography and shapes. To set up Compose for TV dependencies, follow [Compose for TV setup](references/android/training/tv/playback/compose/index.md). -- **Focus zoom animation** : For interactive cards, you must use `CompactCard`, `ClassicCard`, or `WideCardContainer` with `scale = CardDefaults.scale(focusedScale = 1.1f)` to provide standard TV focus animation. -- **Coil image loading** : To use declarative `AsyncImage(model, contentDescription, ...)` without passing an explicit `ImageLoader` parameter, you must include `io.coil-kt:coil-compose` in your Gradle dependencies. -- **Explicit imports** : You must always import TV Material 3 classes explicitly (for example, `import androidx.tv.material3.Surface`, `import androidx.tv.material3.ListItem`) instead of using wildcard imports (`import androidx.tv.material3.*`). +- **Focus zoom animation** : For interactive cards, you must use `CompactCard`, `ClassicCard`, or `WideCardContainer` with `scale = + CardDefaults.scale(focusedScale = 1.1f)` to provide standard TV focus animation. +- **Coil image loading** : To use declarative `AsyncImage(model, + contentDescription, ...)` without passing an explicit `ImageLoader` parameter, you must include `io.coil-kt:coil-compose` in your Gradle dependencies. +- **Explicit imports** : You must always import TV Material 3 classes explicitly (for example, `import androidx.tv.material3.Surface`, `import + androidx.tv.material3.ListItem`) instead of using wildcard imports (`import + androidx.tv.material3.*`). - **File naming conventions** : You must name Composable screen files after the screen (for example, name `BrowseScreen` as `BrowseScreen.kt`, `PlaybackScreen` as `PlaybackScreen.kt`, and `AuthenticationScreen` as `AuthenticationScreen.kt`). Don't use generic prefixes like `Main`. - **Overscan and bezels** : You must apply horizontal padding (for example, `horizontal = 48.dp` or `32.dp`, `vertical = 24.dp`) to root containers, carousels, and top bars to prevent clipping. - **Reading width constrainment** : You must constrain reading width using `Modifier.widthIn(max = 600.dp)` on text columns for long-form text. @@ -58,13 +65,19 @@ When migrating an Android TV application to Jetpack Compose, you must use `andro ## D-pad focus handling and navigation -Jetpack Compose for TV (`androidx.tv.material3`) requires explicit focus management, as components don't receive initial focus automatically and navigation uses 2D spatial coordinates. To configure TV D-pad navigation, follow instructions in [TV Navigation guide](references/android/training/tv/get-started/navigation.md). +Jetpack Compose for TV (`androidx.tv.material3`) requires explicit focus +management, as components don't receive initial focus automatically and +navigation uses 2D spatial coordinates. To configure TV D-pad navigation, follow +instructions in [TV Navigation guide](references/android/training/tv/get-started/navigation.md). ### Initial focus -You must assign initial focus to the primary interactive element on every screen (such as the first action button, card, or `ListItem`) using `FocusRequester` when entering a screen. Define `val focusRequester = remember { FocusRequester() }`, attach `Modifier.focusRequester(focusRequester)` to the primary element, and request focus inside `LaunchedEffect(Unit) { focusRequester.requestFocus() }`: +You must assign initial focus to the primary interactive element on every screen +(such as the first action button, card, or `ListItem`) using `FocusRequester` +when entering a screen. Define `val focusRequester = remember { FocusRequester() +}`, attach `Modifier.focusRequester(focusRequester)` to the primary element, and +request focus inside `LaunchedEffect(Unit) { focusRequester.requestFocus() }`: -
```kotlin val focusRequester = remember { FocusRequester() } @@ -73,20 +86,29 @@ val focusManager = LocalFocusManager.current LaunchedEffect(Unit) { focusRequester.requestFocus() } - ```
-*Note: For screens with dynamic state or pagers (like `OnboardingScreen` using `HorizontalPager`), you must pass the state key to `LaunchedEffect` (for example `LaunchedEffect(pagerState.currentPage)`) so that focus is re-applied when the page changes.* +*Note: For screens with dynamic state or pagers (like `OnboardingScreen` using +`HorizontalPager`), you must pass the state key to `LaunchedEffect` (for example +`LaunchedEffect(pagerState.currentPage)`) so that focus is re-applied when the +page changes.* ### Bidirectional focus routing and avoiding focus traps -When interactive elements sit on opposite sides of the display, standard 2D spatial navigation fails to find targets across them. This creates focus traps where users are unable to navigate out of an area using the D-pad. +When interactive elements sit on opposite sides of the display, standard 2D +spatial navigation fails to find targets across them. This creates focus traps +where users are unable to navigate out of an area using the D-pad. -For symmetrical, bidirectional D-pad navigation without focus traps, you must rely on Compose's 2D spatial focus engine whenever possible. When connecting adjacent UI elements across scrollable containers (like `LazyColumn` or `LazyRow`), don't set directional overrides (`up = ...`, `down = ...`) targeting individual items inside lazy lists. When an item scrolls off-screen during vertical navigation, its `FocusRequester` becomes uninitialized, throwing `IllegalStateException` during focus searches: +For symmetrical, bidirectional D-pad navigation without focus traps, you must +rely on Compose's 2D spatial focus engine whenever possible. When connecting +adjacent UI elements across scrollable containers (like `LazyColumn` or +`LazyRow`), don't set directional overrides (`up = ...`, `down = ...`) targeting +individual items inside lazy lists. When an item scrolls off-screen during +vertical navigation, its `FocusRequester` becomes uninitialized, throwing +`IllegalStateException` during focus searches: -
```kotlin Row( @@ -100,18 +122,25 @@ Row( modifier = Modifier.focusRequester(topBarFocusRequester) ) { Text("Search") } } - ```
### Row focus recollection (`Modifier.focusRestorer`) -When navigating vertically between horizontal carousels (`LazyRow`), Compose's default 2D spatial focus engine searches along the X coordinate of the focused item. If a user scrolls right in Row 1 (for example to Item 4 at X=800dp) and presses DOWN to navigate to Row 2, spatial routing focuses whatever item sits at X=800dp in Row 2. +When navigating vertically between horizontal carousels (`LazyRow`), Compose's +default 2D spatial focus engine searches along the X coordinate of the focused +item. If a user scrolls right in Row 1 (for example to Item 4 at X=800dp) and +presses DOWN to navigate to Row 2, spatial routing focuses whatever item sits at +X=800dp in Row 2. -To make every row maintain its own recollection of card focus (restoring focus to the previously visited item when revisited), you must attach **`Modifier.focusRestorer`** (with no arguments) directly to the `LazyRow`. Don't pass custom fallback `FocusRequester` lambdas in lazy containers, as calling `requestFocus` on an unattached or off-screen item during rapid D-pad scrolling throws `IllegalStateException`. +To make every row maintain its own recollection of card focus (restoring focus +to the previously visited item when revisited), you must attach +**`Modifier.focusRestorer`** (with no arguments) directly to the `LazyRow`. +Don't pass custom fallback `FocusRequester` lambdas in lazy containers, as +calling `requestFocus` on an unattached or off-screen item during rapid D-pad +scrolling throws `IllegalStateException`. -
```kotlin LazyRow( @@ -148,20 +177,26 @@ LazyRow( ) } } - ```
### Text input, hardware keyboard enter interception, and IME focus chaining -When migrating search bars or login forms from Leanback (`SearchSupportFragment`, `GuidedStepSupportFragment`), don't use bare `BasicTextField` containers or empty `Surface(onClick = {})` wrappers, as they prevent D-pad CENTER from attaching the virtual keyboard (IME). +When migrating search bars or login forms from Leanback +(`SearchSupportFragment`, `GuidedStepSupportFragment`), don't use bare +`BasicTextField` containers or empty `Surface(onClick = {})` wrappers, as they +prevent D-pad CENTER from attaching the virtual keyboard (IME). -1. **Clickable TV surface wrapper with Back-key interception (`onPreviewKeyEvent`)** : Wrap standard M3 `TextField` inside a focusable TV `Surface(onClick = { focusRequester.requestFocus() }, scale = ClickableSurfaceDefaults.scale(focusedScale = 1.01f), border = ClickableSurfaceDefaults.border(focusedBorder = Border(BorderStroke(2.dp, Color.White))))` to provide a focused border outline and D-pad focus scaling. You must attach `Modifier.onPreviewKeyEvent` on the text field or wrapper to intercept `Key.Back` and `Key.Escape` so the user's able to remove focus from the input field without exiting the screen. +1. **Clickable TV surface wrapper with Back-key interception + (`onPreviewKeyEvent`)** : Wrap standard M3 `TextField` inside a focusable TV `Surface(onClick = { focusRequester.requestFocus() }, scale = + ClickableSurfaceDefaults.scale(focusedScale = 1.01f), border = + ClickableSurfaceDefaults.border(focusedBorder = Border(BorderStroke(2.dp, + Color.White))))` to provide a focused border outline and D-pad focus scaling. You must attach `Modifier.onPreviewKeyEvent` on the text field or wrapper to intercept `Key.Back` and `Key.Escape` so the user's able to remove focus from the input field without exiting the screen. 2. **Why Back-key interception is mandatory** : When editing a text field on Android TV, pressing the D-pad **Back** button normally navigates back and exits the screen. By intercepting `Key.Back` and `Key.Escape` on `KeyUp` in `onPreviewKeyEvent` to stop editing (clearing focus), the user's able to return to D-pad form navigation without being trapped in the text field or accidentally exiting the screen. -3. **IME focus chaining** : For multi-field forms (such as Username and Password in authentication screens), attach `KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Down) })` with `ImeAction.Next` on top fields to route focus down to the next input box, and `ImeAction.Done` on the bottom field to route focus directly to the submit button: +3. **IME focus chaining** : For multi-field forms (such as Username and Password in authentication screens), attach `KeyboardActions(onNext = { + focusManager.moveFocus(FocusDirection.Down) })` with `ImeAction.Next` on top fields to route focus down to the next input box, and `ImeAction.Done` on the bottom field to route focus directly to the submit button: -
```kotlin Surface( @@ -212,21 +247,26 @@ Surface( } ) } - ```
## Lazy containers -When implementing scrollable lists or grids in Jetpack Compose for TV, you must use standard `LazyColumn`, `LazyRow`, and `LazyVerticalGrid` from `androidx.compose.foundation.lazy` and `androidx.compose.foundation.lazy.grid`. For catalog browsing layouts, follow instructions in [Catalog Browser guide](references/android/training/tv/playback/compose/browse.md). +When implementing scrollable lists or grids in Jetpack Compose for TV, you must +use standard `LazyColumn`, `LazyRow`, and `LazyVerticalGrid` from +`androidx.compose.foundation.lazy` and `androidx.compose.foundation.lazy.grid`. +For catalog browsing layouts, follow instructions in [Catalog Browser guide](references/android/training/tv/playback/compose/browse.md). -1. **Pivot scrolling with `BringIntoViewSpec`** : When defining a custom pivot scroll line for catalog rows using `BringIntoViewSpec` and `CompositionLocalProvider(LocalBringIntoViewSpec provides ...)`, you must ensure your project compiles against Compose Foundation 1.7.0+ by adding `implementation platform('androidx.compose:compose-bom:2024.06.00')` (or newer) or `implementation 'androidx.compose.foundation:foundation:1.7.0'` in `app/build.gradle`. Without Compose Foundation 1.7.0+, `import androidx.compose.foundation.gestures.LocalBringIntoViewSpec` will fail with `Unresolved reference: LocalBringIntoViewSpec`. -2. **Row focus recollection (`Modifier.focusRestorer`)** : Annotate your composable with `@OptIn(ExperimentalFocusRestorerApi::class, ExperimentalComposeUiApi::class)` and attach `Modifier.focusRestorer` on every category `LazyRow` to remember and restore the last focused card when navigating vertically across catalog rows. +1. **Pivot scrolling with `BringIntoViewSpec`** : When defining a custom pivot scroll line for catalog rows using `BringIntoViewSpec` and `CompositionLocalProvider(LocalBringIntoViewSpec provides ...)`, you must ensure your project compiles against Compose Foundation 1.7.0+ by adding `implementation platform('androidx.compose:compose-bom:2024.06.00')` (or newer) or `implementation 'androidx.compose.foundation:foundation:1.7.0'` in `app/build.gradle`. Without Compose Foundation 1.7.0+, `import + androidx.compose.foundation.gestures.LocalBringIntoViewSpec` will fail with `Unresolved reference: LocalBringIntoViewSpec`. +2. **Row focus recollection (`Modifier.focusRestorer`)** : Annotate your composable with `@OptIn(ExperimentalFocusRestorerApi::class, + ExperimentalComposeUiApi::class)` and attach `Modifier.focusRestorer` on every category `LazyRow` to remember and restore the last focused card when navigating vertically across catalog rows. -When populated with focusable TV Material 3 components (`CompactCard`, `ListItem`, `Button`), standard Compose lazy containers handle 2D D-Pad focus routing, focus memory, and edge scrolling automatically: +When populated with focusable TV Material 3 components (`CompactCard`, +`ListItem`, `Button`), standard Compose lazy containers handle 2D D-Pad focus +routing, focus memory, and edge scrolling automatically: -
```kotlin @Composable @@ -246,14 +286,16 @@ fun CatalogBrowser( } } } - ```
## Media3 video playback and transport controls -When migrating legacy Leanback video playback (`VideoSupportFragment` / `PlaybackGlue`), use Compose Media3 `PlayerSurface` (`androidx.media3.ui.compose.PlayerSurface`) combined with a translucent transport controls overlay: +When migrating legacy Leanback video playback (`VideoSupportFragment` / +`PlaybackGlue`), use Compose Media3 `PlayerSurface` +(`androidx.media3.ui.compose.PlayerSurface`) combined with a translucent +transport controls overlay: 1. **Mandatory Media3 transport control buttons (`PlayPauseButton`)** : Over the `PlayerSurface`, you must layer a translucent bottom controls bar (`Box(modifier = Modifier.align(Alignment.BottomCenter))`) containing explicit Media3 UI Compose buttons: at minimum `PlayPauseButton`, `SeekBackButton`, and `SeekForwardButton`. Never leave the transport controls overlay empty. To use these composables, you must add `implementation 'androidx.media3:media3-ui-compose-material3:1.6.0'` alongside `androidx.media3:media3-ui-compose` in your `app/build.gradle` dependencies. 2. **D-pad directional seeking (`onPreviewKeyEvent`)** : To support seeking backward and forward with the remote control D-pad, attach `Modifier.onPreviewKeyEvent` on the container or controls overlay and intercept Compose `Key.DirectionLeft` and `Key.DirectionRight` (for example, `keyEvent.key == Key.DirectionLeft`) to seek backward and forward by 10 seconds (`exoPlayer.seekTo(exoPlayer.currentPosition - 10000)`). Never use legacy Android View keycodes (`KeyEvent.KEYCODE_DPAD_LEFT` or `nativeKeyEvent.keyCode`). @@ -261,7 +303,8 @@ When migrating legacy Leanback video playback (`VideoSupportFragment` / `Playbac ## Phased migration strategy -To migrate an app cleanly without breaking compilation or introducing circular dependencies, execute in five distinct phases: +To migrate an app cleanly without breaking compilation or introducing circular +dependencies, execute in five distinct phases: - **Phase 1**: Foundation and design system - **Phase 2**: Leaf and standalone screens @@ -282,7 +325,6 @@ To migrate an app cleanly without breaking compilation or introducing circular d - Replace legacy `Fragment` classes with activities of type `ComponentActivity` that declaratively use components in Compose. - Clean up legacy style and theme references in `res/values/styles.xml` and `res/values/themes.xml` (such as removing `preferenceTheme` that points to `@style/PreferenceThemeOverlay.v14.Leanback`) that aren't supported once leanback dependencies are removed: -
```kotlin @Composable @@ -339,7 +381,6 @@ fun TvSettingsScreen( } } } - ```
@@ -379,11 +420,16 @@ fun TvSettingsScreen( ### Modern TV immersive list architecture (`BrowseScreen`) -When building a 10-foot TV browse screen or Immersive List, don't place a hero banner before or outside a scrolling list, and don't fight Compose's automatic `BringIntoView` system with programmatic `animateScrollToItem` calls. +When building a 10-foot TV browse screen or Immersive List, don't place a hero +banner before or outside a scrolling list, and don't fight Compose's automatic +`BringIntoView` system with programmatic `animateScrollToItem` calls. -Instead, use `BringIntoViewSpec` with `LocalBringIntoViewSpec` from Compose Foundation to define exact TV pivot scrolling (for example, pivoting active rows at 35% from the top edge of the display). Combine this with a reshaping immersive row: when lower rows are focused (`focusedCategoryIndex > 0`), hide the hero text and display a normal section header on Row 0: +Instead, use `BringIntoViewSpec` with `LocalBringIntoViewSpec` from Compose +Foundation to define exact TV pivot scrolling (for example, pivoting active rows +at 35% from the top edge of the display). Combine this with a reshaping +immersive row: when lower rows are focused (`focusedCategoryIndex > 0`), hide +the hero text and display a normal section header on Row 0: -
```kotlin @Composable @@ -521,16 +567,19 @@ private fun PositionFocusedItemInLazyLayout( } CompositionLocalProvider(LocalBringIntoViewSpec provides bringIntoViewSpec, content = content) } - ```
### Media3 playback in Compose TV (`PlaybackScreen`) -Implement a custom playback screen using `androidx.media3.ui.compose.PlayerSurface` as the video rendering canvas. Layer Material3 transport controls (`SeekBackButton`, `PlayPauseButton`, `SeekForwardButton` from `androidx.media3:media3-ui-compose-material3`) over the surface in a translucent bottom overlay, and handle D-pad remote key events with an auto-hide timeout: +Implement a custom playback screen using +`androidx.media3.ui.compose.PlayerSurface` as the video rendering canvas. Layer +Material3 transport controls (`SeekBackButton`, `PlayPauseButton`, +`SeekForwardButton` from `androidx.media3:media3-ui-compose-material3`) over the +surface in a translucent bottom overlay, and handle D-pad remote key events with +an auto-hide timeout: -
```kotlin @OptIn(UnstableApi::class) @@ -647,16 +696,16 @@ fun Media3PlaybackScreen( } } } - ```
### Replacing CursorLoader with reactive coroutine flow -Replace legacy `LoaderManager.LoaderCallbacks` and `CursorObjectAdapter` with a repository returning a `Flow` that observes database changes and triggers asynchronous fetching when empty: +Replace legacy `LoaderManager.LoaderCallbacks` and `CursorObjectAdapter` +with a repository returning a `Flow` that observes database changes and triggers +asynchronous fetching when empty: -
```kotlin object VideoFlowRepository { @@ -686,7 +735,6 @@ object VideoFlowRepository { return emptyList() } } - ```
diff --git a/tv/leanback-to-compose-tv-migration/references/android/training/tv/get-started/navigation.md b/tv/leanback-to-compose-tv-migration/references/android/training/tv/get-started/navigation.md index 69b9ba2..57815a2 100644 --- a/tv/leanback-to-compose-tv-migration/references/android/training/tv/get-started/navigation.md +++ b/tv/leanback-to-compose-tv-migration/references/android/training/tv/get-started/navigation.md @@ -1,26 +1,45 @@ -TV devices provide a limited set of navigation controls for apps. Creating an effective navigation scheme for your TV app depends on understanding these limited controls as well as users' limitations while operating your app. As you build your Android app for TV, pay special attention to how the user navigates when using remote control buttons instead of a touch screen. +TV devices provide a limited set of navigation controls for apps. Creating an +effective navigation scheme for your TV app depends on understanding these +limited controls as well as users' limitations while operating your app. +As you build your Android app for TV, pay special attention to how the user +navigates when using remote control buttons instead of a touch screen. ## Principles -The goal is for navigation to feel natural and familiar without dominating the user interface or diverting attention from content. The following principles help set a baseline for a consistent and intuitive user experience across TV apps. +The goal is for navigation to feel natural and familiar without dominating the user +interface or diverting attention from content. The following principles help set +a baseline for a consistent and intuitive user experience across TV apps. **Efficient** -Make it fast and easy to get to content. Users want to access content quickly, using a minimal number of clicks. Organize your information in a way that requires the fewest screens. + +Make it fast and easy to get to content. Users want to access content +quickly, using a minimal number of clicks. Organize your information in +a way that requires the fewest screens. **Predictable** -Follow best practices and recommendations to make navigation predictable to users. Don't reinvent navigation patterns unnecessarily, as this leads to confusion and unpredictability. + +Follow best practices and recommendations to make navigation +predictable to users. Don't reinvent navigation patterns +unnecessarily, as this leads to confusion and unpredictability. **Intuitive** -Make navigation simple enough to seamlessly support widely adopted user behaviors. Don't over-complicate by adding unnecessary layers of navigation. + +Make navigation simple enough to seamlessly support widely adopted user +behaviors. Don't over-complicate by adding unnecessary layers of navigation. ## Controllers -Controllers come in a variety of styles, from a minimalist remote control to complex game controllers. All controllers include a directional pad (D-pad) plus select, home, and back buttons. Other buttons vary by model. +Controllers come in a variety of styles, from a minimalist remote control to +complex game controllers. All controllers include a directional pad (D-pad) plus +select, home, and back buttons. Other buttons vary by model. ![Sample Remote](https://developer.android.com/static/training/tv/images/tv-nav-controller.png) **Figure 1.** Example of a TV remote. **D-pad** -The primary navigation method on TV is through the D-pad, which includes up, down, left, and right directional hardware buttons. The D-pad transfers focus from one object to the nearest object in the direction of the button pressed. +The primary navigation method on TV is through the D-pad, which includes +up, down, left, and right directional +hardware buttons. The D-pad transfers focus from one object to the nearest +object in the direction of the button pressed. **Select button** @@ -41,15 +60,23 @@ Invokes either Google Assistant or voice input.
> [!NOTE] -> **Note:** The remote control shown in figure 1 is for reference only. There are many layouts and styles of remotes and controllers, though all of them have the basic functionality described here. For more information, see [Manage TV controllers](https://developer.android.com/training/tv/get-started/controllers). +> **Note:** The remote control shown in figure 1 is for reference only. There are many layouts and styles of remotes and controllers, though all of them have the basic functionality described here. For more information, see [Manage +> TV controllers](https://developer.android.com/training/tv/get-started/controllers). ## D-pad navigation -On a TV device, users navigate using either a D-pad or arrow keys. This type of control limits movement to up, down, left, and right. To build a great TV-optimized app, you must provide a navigation scheme where the user can quickly learn how to navigate your app using these limited controls. +On a TV device, users navigate using either a D-pad or arrow +keys. This type of control limits movement to up, down, left, and right. To +build a great TV-optimized app, you must provide a navigation scheme where the +user can quickly learn how to navigate your app using these limited controls. -The Android framework handles directional navigation between layout elements automatically, so you typically do not need to do anything extra for your app. However, you should thoroughly test navigation with a D-pad controller to discover any navigation problems. +The Android framework handles directional navigation between layout elements +automatically, so you typically do not need to do anything extra for your app. +However, you should thoroughly test navigation with a D-pad controller to +discover any navigation problems. -Follow these guidelines to test that your app's navigation system works well with a D-pad on a TV device: +Follow these guidelines to test that your +app's navigation system works well with a D-pad on a TV device: - Ensure that a user with a D-pad controller can navigate to all visible controls on the screen. - For scrolling lists with focus, make sure that the D-pad up and down buttons scroll the list and that the select button selects an item in the list. Verify that users can select an element in the list and that the list still scrolls when an element is selected. @@ -57,16 +84,22 @@ Follow these guidelines to test that your app's navigation system works well wit ### Modify directional navigation -The Android framework automatically applies a directional navigation scheme based on the relative position of focusable elements in your layouts. Test the generated navigation scheme in your app using a D-pad controller. After testing, if you decide that you want users to move through your layouts in a specific way, you can set up explicit directional navigation for your controls. +The Android framework automatically applies a directional navigation scheme +based on the relative position of focusable elements in your layouts. Test +the generated navigation scheme in your app using a D-pad controller. After +testing, if you decide that you want users to move through your layouts in a specific +way, you can set up explicit directional navigation for your controls. > [!NOTE] > **Note:** Only use these attributes to modify the navigation order if the default order that the system applies does not work well. -The following code sample shows how to define the next control to receive focus for a `TextView` layout object: +The following code sample shows how to define the next control to receive focus +for a `TextView` layout object: -The following table lists all of the available navigation attributes for Android user interface widgets: +The following table lists all of the available navigation attributes for Android +user interface widgets: | Attribute | Function | |---|---| @@ -75,65 +108,97 @@ The following table lists all of the available navigation attributes for Android | [`nextFocusRight`](https://developer.android.com/reference/android/R.attr#nextFocusRight) | Defines the next view to receive focus when the user navigates right. | | [`nextFocusUp`](https://developer.android.com/reference/android/R.attr#nextFocusUp) | Defines the next view to receive focus when the user navigates up. | -To use one of these explicit navigation attributes, set the value to the `android:id` of another widget in the layout. Be sure to set up the navigation order as a loop, so that the last control directs focus back to the first one. +To use one of these explicit navigation attributes, set the value to the +`android:id` of another widget in the layout. Be sure to set up the +navigation order as a loop, so that the last control directs focus back to the +first one. ### Provide clear focus and selection -The success of an app's navigation scheme on TV devices depends on how easy it is for a user to determine what user interface element is in focus. If you do not provide a clear indication of the focused item, and therefore what item a user can take action on, they can quickly become frustrated and exit your app. For the same reason, it is important to always have an item in focus that a user can take action on immediately after your app starts or any time it is idle. +The success of an app's navigation scheme on TV devices depends on how easy it +is for a user to determine what user interface element is in focus. If +you do not provide a clear indication of the focused item, and therefore what item a +user can take action on, they can quickly become frustrated and exit your app. +For the same reason, it is important to always have an item in focus that a user +can take action on immediately after your app starts or any time it is idle. -In your app layout and implementation, use color, size, animation, or a combination of these attributes to help users easily determine what actions they can take next. Use a uniform scheme for indicating focus across your application. +In your app layout and implementation, use color, size, animation, or a +combination of these attributes to help users easily determine what actions they +can take next. Use a uniform scheme for indicating focus across your +application. -Android provides [Drawable state list resources](https://developer.android.com/guide/topics/resources/drawable-resource#StateList) to implement highlights for focused and selected controls. The following code example demonstrates how to enable visual behavior for a button to indicate that a user has navigated to the control and then selected it: +Android provides [Drawable state list resources](https://developer.android.com/guide/topics/resources/drawable-resource#StateList) +to implement highlights for focused and selected controls. The following code +example demonstrates how to enable visual behavior for a button to indicate that +a user has navigated to the control and then selected it: - - - - - - - - -The following layout XML sample code applies the previous state list drawable to a `Button`: +```xml + + + + + + + +``` + +
+ +The following layout XML sample code applies the previous state list drawable to +a `Button`: